Flutter 学习日志 (二) 现有项目集成 flutter

本文主要介绍怎样在现有的 Android 项目中集成 flutter 开发界面,以及 flutter 与原生之间怎样进行方法互调、数据双向传递

依赖集成

官方目前没有正式的集成文档,只有一个 github 上的 wiki 页面做了相关介绍。原因嘛,看了 wiki 就知道了

This page documents ongoing experiments and will be updated as we find more and better ways to do this, and as we build out tooling to aid.

This support is in preview, and is generally only available in the master channel.

目前现有工程集成 flutter 还处在开发探索阶段。

注意

在现有的 Android 项目中集成 flutter 是以 module 的方式依赖到项目中。但 flutter 项目是创建在Android 工程的同级目录下的,而不是像一般 Android lib module 一样跟 app module 在同级目录,譬如要为 D:\AndroidSpace\xxx\AndroidProject工程集成 flutter 则 flutter module 也需要创建在 D:\AndroidSpace\xxx\ 目录下。

集成步骤

1、命令行/powershell 进入到 Android 工程所在的目录,即上文举例中的 xxx 目录,执行 flutter 命令创建 flutter module。

flutter create -t module flutter_module

2、待创建完毕后,打开 Android Project 的 setting.gradle 文件,添加如家代码

setBinding(new Binding([gradle: this]))                       
evaluate(new File( 
        settingsDir.parentFile,
        'flutter_check/.android/include_flutter.groovy'
))

3、同步设置后在 app gradle 中添加依赖(flutter 中的 support 包可能会有依赖冲突,加入 exclude 排除掉 support 包依赖即可解决)

dependencies {
    ...
    implementation(project(':flutter'),{
        exclude group: 'com.android.support'
    })
    ...

4、经过上面的配置 flutter 已经集成到现有 Android 工程中了,可以将 flutter 页面当做 View 添加到原生界面上,并在界面的生命周期函数中调用 FlutterView 的对应生命周期方法。也可以创建一个 FlutterFragment 显示在界面上,由 Flutter 自行处理生命周期相关问题。

  • 添加 flutterview 方式
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT,
                FrameLayout.LayoutParams.MATCH_PARENT);
        flutterView = Flutter.createView(this, getLifecycle(), "PhoneCheck");
        addContentView(flutterView, layout);
        ...
    }
  • FlutterFragment 方式
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flutter_test);
        ...
        FragmentTransaction tx = getSupportFragmentManager().beginTransaction();
        tx.replace(R.id.fl_flutter_container, Flutter.createFragment("PhoneCheck"));
        tx.commit();
        ...
    }

当然,上面两种方式不是必须在 oncreate 中调用,当做普通的 View / fragment 在合适的地方使用就行。使用 Flutterfragment 时如果需要 flutter 与原生间进行通信则需要继承 Flutterfragment 重新 onCreateView() 拿到自动创建的 FlutterView 对象。因为初始化通信信道时需要用到 FlutterView 对象。另外在使用时还遇到了 getLifecycle() 方法找不到的问题,原因是 Android 工程的 buildToolsVersion 太低,之前是 25 后面改成 27 的就解决了。

Flutter 与原生的方法互调及数据传递

通过上面的步骤,flutter 页面是可以嵌入显示在原生应用上了。但现阶段还是各显示各的互不交流,这显然是不满足需求的。好在 Flutter 为我们提供了通信的桥梁——平台插件,Flutter 与原生的通信通过两个类型的 Channel 来实现。即通过反射进行方法调用的 MethodChannel 和将数据转换成二进制比特数组直接传递的 BasicMessageChannel ,且通信都是双向的。Channel 支持传输的数据类型有以下:

enter description here

,如果需要传递自定义的对象数据等则需要转成 json 字符串或 map 对象传递过去再解析。

MethodChannel

MethodChannel 是 Flutter 与平台原生进行方法互调的插件,Android 中通过反射实现方法调用。

example

flutter module 对应的 State 中

    ...
    MethodChannel platform;
  @override
  void initState() {
    ...
    platform = const MethodChannel('my_flutter/plugin');
    platform.setMethodCallHandler(methodHandler);
    super.initState();
    ...
  }
  
   // 接收原生平台的方法调用处理
  Future methodHandler(MethodCall call) async {
    ...
  }

初始化 WidgetState 的时候初始化 MethodChannel 并为之设置回调处理方法,回调方法中有参数 MethodCall,可通过 call.method 来判断需要执行的具体操作。
Android 原生代码
与 flutter 端一样,也需要先初始化 channel

MethodChannel mChannel;
private void initChannel() {
        mChannel = new MethodChannel(flutterView, "my_flutter/plugin");
        
        mChannel.setMethodCallHandler((MethodCall methodCall, MethodChannel.Result result) -> {
            switch (methodCall.method) {
                case "action1":
                    ...
                    break;
                case "action2":
                    ...
                    result.success("jump");
                    break;
            }
        });
    }

调用方式

  • flutter 端调用原生中的方法:
    // flutter 中有一个 getheaders 方法,从原生去获取 headers map 对象
Map<String, String> headers;
  getHeaders() async {
    try {
      Map<dynamic, dynamic> result = await platform.invokeMethod('getHeaders');
      // 将 dynamic 类型转换成需要的类型(与平台端返回值给定的类型要一致)
      headers = result.cast();
    } on PlatformException catch (e) {
      print(e.message);
    }
  }

在 flutter 中使用 MethodChannel 对象执行 invoke(‘methodName’)即可调用,如方法需要传递参数则调用 invoke(‘methodName’[dynamic]),传递一个 dynamic 类型的参数数组。在原生平台中使用 methodCall.arguments()获取参数。 需要返回值则需要添加 await 修饰。原生平台中的 MethodCallHandler 回调就会接收到调用信息,methodCall.method值为 flutter 端 invoke 的方法名,result 对象设置的返回值则为 flutter 平台获取到的返回值

mChannel.setMethodCallHandler((MethodCall methodCall, MethodChannel.Result result) -> {
            switch (methodCall.method) {
                case "action1":
                    ...
                    break;
                case "action2":
                    ...
                    result.success("jump");
                    break;
            }
        });
  • 原生平台调用 flutter 方法
//与 flutter 调用原生一样,使用 MethodChannel 的 invoke 方法
mChannel.invokeMethod("setAddress");
// 调用并传递参数
mChannel.invokeMethod("setAddress",Object);
  Future methodHandler(MethodCall call,) async {
    print(call.method);
    if (call.method == 'setAddress') {
        ...
    }
  }

flutter 中的 methodHandler 接收到方法调用,再做相关处理。与 flutter 调用原生不一样,原生调用 fultter 方法是没有返回值的。

注意事项

  • MethodChannel 初始化时,指定的 channelName 参数,如上面代码片段中的my_flutter/plugin。需要 flutter 端与原生平台端一致,才能保证 Channel 信道一致,互相接收到对放发送的方法调用请求。
  • 参数传递时,传递的参数为基本数据类型或者 Map List 之类的双方都能识别的类型,自定义数据类转换成 map 或者 json 字符串再传递。当 flutter 传递的参数为 json对象(内部也是转成 map 传递的) 或者 map 时可以在原生中可以直接使用 methodCall.argument("key");获取参数值,获取前可以使用 methodCall.hasArgument("key");判断参数是否存在。

BasicMessageChannel

BasicMessageChannel 是 Flutter 与平台原生直接发送和接收数据的插件,数据以二进制的形式在其中传输。
基本步骤和 MethodChannel 一致,在 flutter 端和平台端都实例化对象并设置回调,与 MethodChannel 初始化不同之处在于初始化时多了一个类型参数,因为 BasicMessageChannel 本身是可以传入泛型的,因此在初始化时需要指定泛型类型。其实打开 MethodChannel 的初始化方法会发现,内部也是传入了类型的,只不过使用的默认值 StandardMethodCodec.INSTANCE。具体有哪些类型实现,打开 project 依赖目录找到 flutter.jar 其中的 io.flutter.common 包中可以看到

Class Note
BinaryCodec 二进制数据类型,泛型对应 ByteBuffer,传递 byte 数组时使用
JSONMessageCodec Json消息类型,泛型对应 Object。传递 Json时可使用
StringCodec 字符串消息类型,泛型对应 String。传递字符串
StandardMessageCodec 标准消息类型,泛型对应 Object。传递 Map 等的时候可使用此类型

Flutter 中对应 class 在 flutter\src\services\message_codecs.dart 文件中。

总结

本文只是记录对照官方文档上手在 demo 中实践的心得,可能会理解有所偏差。待深入研究再进行纠正。flutter 与原生平台通信主要通过三个默认的 Channel ,除了上面说到的 MethodChannelBasicMessageChannel还有一个 EventChannel具体可以看一下这篇文章

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

推荐阅读更多精彩内容