android原生混编Flutter

花了一天时间初步进行接入,有些坑还没有踩全。包括第一次加载flutter界面的时候显示很慢等。
以下是代码集成方式,后续探索产物集成方式。


image

接入步骤

  • 第一步、新建android原生项目

  • 第二步、新建Flutter Module

    1. 通过命令行创建。切换到android项目的同级目录下(这里建议直接使用Terminal)。执行如下命令:

      flutter create -t module my_flutter
      

      其中my_flutter为改module名字。

    2. 直接使用AS创建。File --> New --> New Flutter Project,然后选择Flutter Module。然后填写module的名称、路径。最后填写module的包名,点击Finish就创建好了一个Flutter Module。

  • 第三步在android项目中引入 Flutter Module

    1. 在app下的build.gradle文件中添加以下配置

      compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
      }
      

      可以解决版本兼容性问题。如果不配置可能会报错Invoke-customs are only supported starting with Android O (--min-api 26)

    2. 在项目的根目录下的setting.gradle文件中配置

      include ':app'
      // 加入下面配置
      setBinding(new Binding([gradle: this]))
      evaluate(new File(
              settingsDir.parentFile,
              'FlutterDemo/my_flutter/.android/include_flutter.groovy'
      ))
      

      需要修改为自己的module名字。

    3. 编译成功后在app的build.gradle文件下添加依赖。

      implementation project(':flutter')
      

android 与 Flutter交互

Flutter在1.12版本后舍弃了部分类,导致交互这里有较大变动。

android原生跳转到Flutter界面

一、Activity形式

  1. Activity形式。新建一个Android Activity。进行跳转。代码如下:

    public class FlutterPageActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_flutter_page);
            initViews();
        }
    
        private void initViews() {
            FlutterView flutterView = new FlutterView(this);
            FrameLayout.LayoutParams lp =  new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
            FrameLayout flContainer = findViewById(R.id.fl_container);
            flContainer.addView(flutterView, lp);
            FlutterEngine flutterEngine = new FlutterEngine(this);
            flutterEngine.getNavigationChannel().setInitialRoute("route1");
            flutterEngine.getDartExecutor().executeDartEntrypoint(
                    DartExecutor.DartEntrypoint.createDefault()
            );
            flutterView.attachToFlutterEngine(flutterEngine);
        }
    }
    
    • 因为io.flutter.facade,此处采用FlutterView(继承自FrameLayout)替代了原来的Flutter.createView();

    • **attachToFlutterEngine(FlutterEngine flutterEngine) ** 方法的作用就是flutter的ui显示到FlutterView中。

    • FlutterEngine 负责在android端执行Dart代码的引擎。

    • flutterEngine.getNavigationChannel().setInitialRoute("route1"); 设置界面路由。如果不设置默认是“/”界面。

    • 传参可以采用类似get拼接的形式,例如:

      "route1?{\"name\":\"LiLei\"}"
      

      将路由和参数采用?进行隔开,后续可以添加上json字符串。在Flutter 端解析的时候采用window.defaultRouteName 获取路由名称和参数。

      String url = window.defaultRouteName;
      // route名称
      String route =
          url.indexOf('?') == -1 ? url : url.substring(0, url.indexOf('?'));
      // 参数Json字符串
      String paramsJson =
          url.indexOf('?') == -1 ? '{}' : url.substring(url.indexOf('?') + 1);
      // 解析参数
      Map<String, dynamic> params = json.decode(paramsJson);
      
    • 在flutter中路由的简单写法。

      void main() => runApp(_widgetForRoute(window.defaultRouteName));
      Widget _widgetForRoute(String route) {
        switch (route) {
          case 'route1':
            return MyApp();
          default:
            return Center(
              child: Text('Unknown route: $route', textDirection: TextDirection.ltr),
            );
        }
      }
      
  2. 创建好Activity后原生界面A 就可以采用Intent的方式进行跳转。

    Intent intent = new Intent(MainActivity.this, FlutterPageActivity.class);
    startActivity(intent);
    

二、Fragment形式

  1. 新建一个Fragment 继承自Fragment。

  2. 在fragment的onCreate方法中创建FlutterView。具体方法和Activity中相同。

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
       FlutterView flutterView = new FlutterView(getContext());
       FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
       mFlutterEngine = new FlutterEngine(getContext());
       mFlutterEngine.getNavigationChannel().setInitialRoute("route1");
       mFlutterEngine.getDartExecutor().executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
       );
       flutterView.attachToFlutterEngine(mFlutterEngine);
       return flutterView;
     }
    
    • 这里将FlutterEngine方法设置为成员变量为了后续界面间传值。
  3. 在原生Activity中添加加载方法。

    public class FlutterFragmentActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_flutter_fragment);
            initViews();
        }
    
        private void initViews() {
            //写法1
            getSupportFragmentManager().beginTransaction().replace(R.id.fl_container, new FlutterPageFragment()).commit();
    
            //写法2
            FlutterFragment fragment = FlutterFragment.withNewEngine().initialRoute("route1").build();
            getSupportFragmentManager().beginTransaction().replace(R.id.fl_container, fragment).commit();
    
            //写法3
            //通过FlutterFragment引入Flutter编写的页面
            FlutterFragment flutterFragment = FlutterFragment.withNewEngine()
                    .initialRoute("route2")
                    .build();
            getSupportFragmentManager()
                    .beginTransaction()
                    .replace(R.id.fl_container, flutterFragment)
                    .commit();
        }
    }
    
    • 写法3中 在androidx环境中 flutterFragment需要继承自androidx的fragment。

Flutter界面跳转到Android原生界面

  1. 在布局中添加一个按钮。

    static const nativeChannel = const MethodChannel('com.example.flutter/native');
    

    首先要定义一个channel,全局唯一,需要和android端约定好。

    RaisedButton(
         child: Text('跳转到原生界面'),
         onPressed:() {
         // 返回给上一页的数据
         Map<String, dynamic> result = {'name': '我从Flutter页面过来了'};
         nativeChannel.invokeMethod('jumpToNative', result);
    }),
    
    • dart采用map形式发送。定义好key value
    • 通过nativeChannel.invokeMethod('jumpToNative', result); 方法传递出去。其中第一个参数是和android端预定好的方法名。
  2. 在fragment中进行接收

    private static final String CHANNEL_NATIVE = "com.example.flutter/native";
    
    @Override
        public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            MethodChannel nativeChannel = new MethodChannel(mFlutterEngine.getDartExecutor(), CHANNEL_NATIVE);
            nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
                @Override
            public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
                    switch (methodCall.method) {
                        case "jumpToNative":
                            // 跳转原生页面
                            Intent jumpToNativeIntent = new Intent(getActivity(), NativeActivity.class);
                            jumpToNativeIntent.putExtra("name", (String) methodCall.argument("name"));
                             //因为写的demo所以直接采用了魔法数字 方便文章中看的直观
                            startActivityForResult(jumpToNativeIntent, 1001);
                            break;
                        default:
                            result.notImplemented();
                            break;
                    }
                }
            });
        }
    

    在实现Flutter页面跳转Android原生页面之前首先介绍一下Platform Channel,它是Flutter和原生通信的工具,有三种类型:

    • BasicMessageChannel:用于传递字符串和半结构化的信息,Flutter和平台端进行消息数据交换时候可以使用。
    • MethodChannel:用于传递方法调用(method invocation),Flutter和平台端进行直接方法调用时候可以使用。
    • EventChannel:用于数据流(event streams)的通信,Flutter和平台端进行事件监听、取消等可以使用。

    Flutter 跳转到原生界面主要通过MethodChannel来实现。

    • 在创建MethodChannel的时候需要传入一个常量。需要保证唯一性。和dart端约定好采用一样的常量。就是上文中提到的nativeChannel
    • 第一个参数是BinaryMessenger类型。需要通过mFlutterEngine.getDartExecutor()方法获取到。
      • 这里有两个前提:
        • a. 需要界面加载完成后。所以我们写在onViewCreated中;
        • b. 需要从flutterEngine中获取,所以我们将该变量改成成员变量。
    • 通过setMethodCallHandler回调方式判断返回的方法名。在进行相应处理。
    • 获取到想要的方法名之后,使用intent将获取到的数据发送出去。
    • 这里采用startActivityForResult 方法是因为在原生界面中做了数据回调。

从android原生界面返回到Flutter界面数据传递

  1. 接上面的跳转原生界面成功后,在原生界面中添加按钮进行返回并传值。

    tv_demo.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    Intent intent = new Intent();
                    intent.putExtra("message", "我从原生页面回来了");
                    setResult(RESULT_OK, intent);
                    finish();
                }
    });
    
  2. 在上诉的fragment的onActivityResult方法中进行接收。

    @Override
    public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if(resultCode == Activity.RESULT_OK){
                Log.e(TAG,"接收到回调结果");
                // NativePageActivity返回的数据
                String message = data.getStringExtra("message");
                Map<String, Object> result = new HashMap<>();
                result.put("message", message);
                // 创建MethodChannel,这里的flutterView即Flutter.createView所返回的View
                MethodChannel flutterChannel = new MethodChannel(mFlutterEngine.getDartExecutor(), CHANNEL_FLUTTER);
                // 调用Flutter端定义的方法
                flutterChannel.invokeMethod("onActivityResult", result);
            }
        }
    
    • 调用了flutter中的方法将值传入到flutter中并显示在界面上。

    • 定义一个全局唯一常量,和flutter中约定好。

      private static final String CHANNEL_FLUTTER = "com.example.flutter/flutter";
      
      static const flutterChannel = const MethodChannel('com.example.flutter/flutter');
      
    • 在initState中进行接收

      String _backResult = "初步设置";
      @override
        void initState() {
          super.initState();
          Future<dynamic> handler(MethodCall call) async {
            switch (call.method) {
              case 'onActivityResult':
              // 获取原生页面传递的参数
                print(call.arguments['message']);
                setState(() {
                  _backResult = call.arguments['message'];
                });
                break;
            }
          }
          flutterChannel.setMethodCallHandler(handler);
        }
      

      用变量来记录

    • 新建一个text来显示这label

      Text(
         '$_backResult'
      ),
      

Flutter界面返回到android原生界面数据传递

  1. 新建一个返回按钮

    RaisedButton(
          child: Text('返回上一页'),
          onPressed: () {
          // 返回给上一页的数据
          Map<String, dynamic> result = {'message': '我从Flutter页面回来了'};
          nativeChannel.invokeMethod('goBackWithResult', result);
    }),
    

    并执行返回的方法。方法名和key提前约定好。

  2. 原生fragmen中先接收相应的参数,然后使用intent进行传递。

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
            super.onViewCreated(view, savedInstanceState);
            MethodChannel nativeChannel = new MethodChannel(mFlutterEngine.getDartExecutor(), CHANNEL_NATIVE);
            nativeChannel.setMethodCallHandler(new MethodChannel.MethodCallHandler() {
                @Override
                public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) {
                    switch (methodCall.method) {
                        case "goBackWithResult":
                            // 返回上一页,携带数据
                            Intent backIntent = new Intent();
                            backIntent.putExtra("message", (String) methodCall.argument("message"));
                            getActivity().setResult(Activity.RESULT_OK, backIntent);
                            getActivity().finish();
                            break;
                        default:
                            result.notImplemented();
                            break;
                    }
                }
            });
        }
    
  1. 在上一个原生界面的activity中进行接收信息。

    @Override
    protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
            super.onActivityResult(requestCode, resultCode, data);
            if (resultCode == RESULT_OK) {
                tv_result.setVisibility(View.VISIBLE);
                String result = data.getStringExtra("message");
                tv_result.setText("result" + result);
            }
     }
    

demo

https://github.com/ShawEw/FlutterAndroidDemo/tree/master

参考

https://www.jianshu.com/p/7b6522e3e8f1
https://www.jianshu.com/p/4a27e091af0b

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

推荐阅读更多精彩内容