Flutter混编(Android)

  • 重0开始新建一个module
  • 利用已有的module
  • 在原生页面中展示Flutter页面
  • flutter和原生通信
  • 使用FlutterBoost实现各种路由需求

其实Flutter的混编方案并非只有一种,但是本文所介绍的是Flutter团队官方给出的方案。它的优缺点我暂不讨论,接下来我们一步一步的实现一个Android项目的混编。

新建module


使用命令新建一个flutter的module,或者也可以使用AndroidStudio直接新建一个flutter module。

flutter create -t module mymodule

然后在app下的build.gradle文件中添加下面两个部分的内容

在android中添加

compileOptions {
  sourceCompatibility 1.8
  targetCompatibility 1.8
}

在dependencies添加flutter项目的依赖

implementation project(':flutter')

在项目的中的setting.gradle文件中添加如下代码

setBinding(new Binding([gradle: this]))
evaluate(new File(
        './mymodule/.android/include_flutter.groovy'))

注意:如果你的最低支持的sdk版本是低于16的话是会报错的,因为flutter最低支持api 16版本。ok如果不出什么其他意外的话集成的步骤已经完成了,可以开始原生和Flutter的混编之旅了。

Flutter页面在Activity中展示


public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        FlutterView flutterView = Flutter.createView(
                MainActivity.this,
                getLifecycle(),
                "route1"
        );
        FrameLayout layout = findViewById(R.id.content);
        layout.addView(flutterView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
    }
}

Flutter页面在Fragment中展示



public class DemoFragmentActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fragment);
        FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
        tx.replace(R.id.someContainer, Flutter.createFragment("route1"));
        tx.commit();
    }
}

或者是这样写

public class DemoFlutterFragment extends Fragment {
   @Override
    public FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return Flutter.createView(getActivity(), getLifecycle(), "route1");
    }
}

这两种方式其实并没有什么区别,只是前者的createFragment方式是帮我们创建了一个FlutterFragment对象,这个FlutterFragment类的路径如下图所示:

image.png

FlutterFragment的源码是这个样子的

public class FlutterFragment extends Fragment {
  public static final String ARG_ROUTE = "route";
  private String mRoute = "/“;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (getArguments() != null) {
      mRoute = getArguments().getString(ARG_ROUTE);
    }
  }
  @Override
  public void onInflate(Context context, AttributeSet attrs, Bundle savedInstanceState) {
    super.onInflate(context, attrs, savedInstanceState);
  }
  @Override
  public FlutterView onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    return Flutter.createView(getActivity(), getLifecycle(), mRoute);
  }
}

如何展示指定的Flutter页面


修改main.dart代码如下,根据传过来的路由名称来展示不同的flutter页面,默认情况是route1

import 'dart:ui';
import 'package:flutter/material.dart';
void main() => runApp(_widgetForRoute(window.defaultRouteName));
Widget _widgetForRoute(String route) {
  switch (route) {
    case 'route1':
      return SomeWidget(...);
    case 'route2':
      return SomeOtherWidget(...);
    default:
      return Center(
        child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
      );
  }
}

如何解决flutter页面第一次加载黑屏的问题


public class DemoActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_demo);
        FlutterView flutterView = Flutter.createView(
                DemoActivity.this,
                getLifecycle(),
                "route1"
        );
        FrameLayout layout = findViewById(R.id.content);
        layout.addView(flutterView, new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
        final FlutterView.FirstFrameListener[] listeners = new FlutterView.FirstFrameListener[1];
        listeners[0] = new FlutterView.FirstFrameListener() {
            @Override
            public void onFirstFrame() {
                layout.setVisibility(View.VISIBLE);
            }
        };
        flutterView.addFirstFrameListener(listeners[0]);
   }
}

使用消息通道和原生通信


这里以一个实际例子来说明问题,比如说flutter中打印日志只能使用print,并没有像Android里面的日志分级调试起来很不方便,这个时候就可以使用消息通道来调用原生的Log。

flutter代码

class LogUtils {

  static const perform = const MethodChannel("android_log");

  static void v(String tag, String message) {

    perform.invokeMethod('logV', {'tag': tag, 'msg': message});
  }
  static void d(String tag, String message) {
    perform.invokeMethod('logD', {'tag': tag, 'msg': message});
  }
  static void i(String tag, String message) {
    perform.invokeMethod('logI', {'tag': tag, 'msg': message});
  }
  static void w(String tag, String message) {
    perform.invokeMethod('logW', {'tag': tag, 'msg': message});
  }
  static void e(String tag, String message) {
    perform.invokeMethod('logE', {'tag': tag, 'msg': message});
  }
}

原生代码,这里使用了kotlin

MethodChannel(flutterView,"android_log").setMethodCallHandler { call, result ->
    logPrint(call)
}
private fun logPrint(call: MethodCall) {
    var tag: String = call.argument("tag")!!
    var message: String = call.argument("msg")!!
    when (call.method) {
        "logV" -> Log.v(tag, message)
        "logD" -> Log.d(tag, message)
        "logI" -> Log.i(tag, message)
        "logW" -> Log.w(tag, message)
        "logE" -> Log.e(tag, message)
    }
}

如何使用呢?在dart代码中调用如下:

LogUtils.v("tag", "v------");
LogUtils.d("tag", "d------");
LogUtils.i("tag", "i------");
LogUtils.w("tag", "w------");
LogUtils.e("tag", "e------");

更多关于消息通道的问题请参考https://flutter.dev/docs/development/platform-integration/platform-channels

怎么使用FlutterBoost做路由


flutter混编之后的路由跳转情况

  • 原生跳原生

  • 原生跳flutter

  • flutter跳原生

  • flutter跳flutter

原生跳原生这个不用说了之前怎么样就是怎么样,原生跳flutter、flutter跳原生、flutter跳flutter这三种情况我们想想要怎么处理呢?原生跳flutter页面我们可以通过原生页面A跳转到原生页面B然后通过原生页面B展示flutterB页面,flutter跳原生可以让flutterA通过消息通道通知原生页面A跳转到原生页面B,那么flutter跳flutter怎么办呢?首先要说的一点是在混编方案中Flutter本身的那套路由跳转(Navigator.push)是不起作用的,这点我们也是在开发新闻开发者这个demo的时候才发现的,那么怎么办呢?最笨的办法是flutterA通知原生页面A跳转原生页面B然后使用原生页面B展示flutterB,但是这样做不仅麻烦而且还消耗内存。原因是原生页面加载一个flutter页面的时候都要初始化一个flutter引擎,而不同页面的flutter引擎资源是不共享的,而且引擎本身也是非常重的。于是FlutterBoost进入我们的视野,FlutterBoost采用的方案是共享引擎模式,换句话说我们不同原生页面可以使用同一个flutter引擎,这样是不是就解决了我们的苦恼?

如何使用?
在Flutter的pubspec.yaml文件中添加依赖

dependencies:
  flutter:
    sdk: flutter
  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.2
  flutter_boost: ^0.0.400//添加flutterboost依赖

或者这样

flutter_boost:
        git:
            url: 'https://github.com/alibaba/flutter_boost.git'
            ref: '0.0.408'

然后在对应的flutter module路径先执行 flutter paskages get命令,然后重新build。

在Flutter端注册FlutterBoost

class MyApp extends StatefulWidget {
  @override
  State<StatefulWidget> createState() {
    return new MyAppState();
  }
}
class MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    ///register page widget builders,the key is pageName
    FlutterBoost.singleton.registerPageBuilders({
      'MyHomePage': (pageName, params, _) => MyHomePage(),
      'FlutterPage': (pageName, params, _) => FlutterPage(),
      'FlutterFragmentPage': (pageName, params, _) => FlutterFragmentPage()
    });
    ///query current top page and load it
    FlutterBoost.handleOnStartPage();
  }
  @override
  Widget build(BuildContext context) => MaterialApp(
      title: 'Flutter Boost example',
      builder: FlutterBoost.init(),//注册FlutterBoost
      ///init container manager
      home: Container());
}

原生方面在app的build.gradle添加flutterboost依赖

implementation project(path: ':flutter_boost')

在Application中注册

FlutterBoostPlugin.init(new IPlatform() {
    @Override
    public Application getApplication() {
        return App.this;
    }
    @Override
    public Activity getMainActivity() {
        return null;//返回MainActivity对象
    }
    @Override
    public boolean isDebug() {
        //表示是否是debug模式
        return false;
    }
    @Override
    public boolean startActivity(Context context, String url, int requestCode) {
        //用于做路由跳转,url是在main.dart中定义好的页面的名称,返回true表示跳转成功,返回false表示失败
        //return false;
        return PageRouter.openPageByUrl(context, url);
    }
    @Override
    public Map getSettings() {
        return null;
    }
});

在Activity中展示Flutter页面

public class FlutterPageActivity extends BoostFlutterActivity {
    @Override
    public String getContainerName() {
        return "FlutterPage";
    }
    @Override
    public Map getContainerParams() {
        //传参数的时候用
        return null;
    }
    @Override
    public void onRegisterPlugins(PluginRegistry registry) {
        GeneratedPluginRegistrant.registerWith(registry);
    }
}

在Fragment中展示Flutter页面

public class DemoFlutterFragment extends BoostFlutterFragment {
    @Override
    public void destroyContainer() {
    }
    @Override
    public String getContainerName() {
        return "FlutterFragmentPage";
    }
    @Override
    public Map getContainerParams() {
        return null;
    }
    @Override
    public void onRegisterPlugins(PluginRegistry registry) {
        GeneratedPluginRegistrant.registerWith(registry)
    }
}

注意:getContainerName()方法返回的是当前Flutter页面的名称,并且这个Flutter页面必须已经被注册这样才能正常展示。

使用FlutterBoost发起路由跳转

FlutterBoost.singleton.openPage(
    "flutter:FlutterPage", {}, animated: true);

自定义协议

注册Flutter页面

FlutterBoost.singleton.registerPageBuilders({
      'xxxPage': (pageName, params, _) => xxxPage(),
    });

发起路由,跳转名字为xxxPage的Flutter页面

FlutterBoost.singleton.openPage(
    "flutter:xxxPage", {}, animated: true);

跳转Native页面

FlutterBoost.singleton.openPage(
    "native:xxxActivity", {}, animated: true);

PageRouter

public class PageRouter {
    public static boolean openPageByUrl(Context context, String url) {
        return openPageByUrl(context, url, 0);
    }
    public static boolean openPageByUrl(Context context, String url, int requestCode) {
        if (url.startsWith("flutter")) {//跳转Flutter
            Intent intent = new Intent(context, FlutterPageActivity.class);
            String[] strings = url.split(":");
            intent.putExtra("url", strings[1]);
            intent.putExtra("requestCode", requestCode);
            try {
                context.startActivity(intent);
                return true;
            } catch (Throwable t) {
                return false;
            }
        } else {
            //跳转native,根据:后面打的字符串来跳转不同的Activity
        }
        return false;
    }
}

FlutterPageActivity代码也做修改如下:

public class FlutterPageActivity extends BoostFlutterActivity {
    String pageRoute;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        pageRoute = getIntent().getStringExtra("url");
    }
    @Override
    public String getContainerName() {
        return pageRoute;
    }
    @Override
    public Map getContainerParams() {
        //传参数的时候用
        return null;
    }
    @Override
    public void onRegisterPlugins(PluginRegistry registry) {
        GeneratedPluginRegistrant.registerWith(registry);
    }
}

这样会使我们代码量减少,如在多个Fragment切换的场景下,遇到闪屏问题可以尝试在onRegisterPlugins方法中添加1如下代码

boostFlutterView.setZOrderOnTop(true)
boostFlutterView.holder.setFormat(PixelFormat.TRANSLUCENT)

参考


https://yq.aliyun.com/articles/693659?spm=a2c4e.11153959.0.0.7d75616br7x3bm

https://www.jianshu.com/p/1397936bbf9b

https://flutter.dev/docs/development/platform-integration/platform-channels

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容