Flutter 49: 图解 Flutter 与 Android 原生交互

      小菜上一篇简单学习了一下 Android 原生接入 Flutter Module,现在学习一下两者之间的数据交互;

      FlutterAndroid/iOS 之间信息交互通过 Platform Channel 进行桥接;Flutter 定义了三种不同的 Channel;但无论是传递方法还是传递事件,其本质上都是数据的传递;

1. MethodChannel:用于传递方法调用;
2. EventChannel:用于数据流信息通信;
3. BasicMessageChannel:用于传递字符串和半结构化的信息;

      每种 Channel 均包含三个成员变量;

  1. name:代表 Channel 唯一标识符,Channel 可以包含多个,但 name 为唯一的;
  2. messager:代表消息发送与接收的工具 BinaryMessenger
  3. codec:代表消息的编解码器;

      小菜以上一节 Android 原生集成 Flutter Module 为基础,针对不同的 Channel 进行学习尝试;且小菜通过 View / Fragment / Activity 三种原生加载方式进行测试;

MethodChannel

      小菜在 Flutter 页面,点击右下角按钮,将消息传递给 AndroidMethodChannel 通过 invokeMethod 进行消息发送,固定的第一个 name 参数是必须存在且唯一的,与 Android 原生中匹配;第二个参数为传送的数据,类似于 Intent 中的 ExtraData,只是支持的数据类型偏少;第三个可隐藏的参数为编解码器;

class _MyHomePageState extends State<MyHomePage> {
  static const methodChannel = const MethodChannel('ace_demo_android_flutter');
  String _result = '';

  Future<Null> _getInvokeResult() async {
    try {
      _result = await methodChannel
          .invokeMethod('ace_demo_user', {'name': '我不是老猪', 'gender': 1});
    } on PlatformException catch (e) {
      _result = "Failed: '${e.message}'.";
    }
    setState(() {});
  }

  void _incrementCounter() {
    setState(() {
      _getInvokeResult();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
            child: Text('${_result}', style: TextStyle(color: Colors.blueAccent, fontSize: 18.0))),
        floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter, child: Icon(Icons.arrow_back)));
  }
}

1. FlutterView

      在 Android 集成 Flutter Module 中时,官方建议使用 View / Fragment 方式,在使用 View 时,建议 Activity 继承 FlutterActivity 或 FlutterFragmentActivity,通过 FlutterView 进行 MethodChannel 绑定监听;

public class MyFlutterViewActivity extends FlutterFragmentActivity {

    private static final String CHANNEL = "ace_demo_android_flutter";
    private static final String TAG = "MyFlutterViewActivity";
    private static final int REQUEST_CODE = 1000;
    FlutterView flutterView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_flutter);

        DisplayMetrics outMetrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(outMetrics);
        int widthPixels = outMetrics.widthPixels;
        int heightPixels = outMetrics.heightPixels;

        flutterView = Flutter.createView(MyFlutterViewActivity.this, getLifecycle(), "/");
        FrameLayout.LayoutParams layout = new FrameLayout.LayoutParams(widthPixels, heightPixels);
        addContentView(flutterView, layout);
        new MethodChannel(flutterView, CHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                if (call.method.equals("ace_demo_user")) {
                    if (call.arguments != null) {
                        Log.e(TAG, "Flutter -> Android 回调内容:" + call.arguments.toString());
                    } else {
                        Log.e(TAG, "Flutter -> Android 回调参数为空!");
                    }
                    result.success("Android -> Flutter 接收回调后返回值:" + TAG);
                    Intent intent = new Intent();
                    intent.putExtra("data", call.arguments!=null?call.arguments.toString():"");
                    setResult(REQUEST_CODE, intent);
                    MyFlutterViewActivity.this.finish();
                } else {
                    result.notImplemented();
                }
            }
        });
    }
}

2. FlutterFragment

      使用 Fragment 方式时与 View 方式均需要获取 FlutterView 进行绑定,此时 Fragment 继承 FlutterFragment 较易获取;

public class MyFlutterFragment extends FlutterFragment {

    private static final String CHANNEL = "ace_demo_android_flutter";
    private static final String TAG = "MyFlutterFragment";

    @SuppressWarnings("unchecked")
    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        new MethodChannel((FlutterView) getView(), CHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, final MethodChannel.Result result) {
                if (call.method.equals("ace_demo_user")) {
                    if (call.arguments != null) {
                        Log.e(TAG, "Flutter -> Android 回调内容:" + call.arguments.toString());
                    } else {
                        Log.e(TAG, "Flutter -> Android 回调参数为空!");
                    }
                    result.success("Android -> Flutter 接收回调后返回值:" + TAG);
                    Toast.makeText(getActivity(), (call.arguments != null) ? "回调内容为:" + call.arguments.toString() : "回调参数为空!", Toast.LENGTH_SHORT).show();
                } else {
                    result.notImplemented();
                }
            }
        });
    }
}

3. FlutterActivity

      使用 Activity 方式同样需要获取 FlutterView 此时直接继承 FlutterActivity 或 FlutterFragmentActivity 即可;

public class MyFlutterActivity extends FlutterActivity {

    private static final String CHANNEL = "ace_demo_android_flutter";
    private static final String TAG = "MyFlutterActivity";
    private static final int REQUEST_CODE = 1000;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        GeneratedPluginRegistrant.registerWith(this);
        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(new MethodChannel.MethodCallHandler() {
            @Override
            public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                if (call.method.equals("ace_demo_user")) {
                    if (call.arguments != null) {
                        Log.e(TAG, "Flutter -> Android 回调内容:" + call.arguments.toString());
                    } else {
                        Log.e(TAG, "Flutter -> Android 回调参数为空!");
                    }
                    result.success("Android -> Flutter 接收回调后返回值:" + TAG);
                    Intent intent = new Intent();
                    intent.putExtra("data", call.arguments!=null?call.arguments.toString():"");
                    setResult(REQUEST_CODE, intent);
                    MyFlutterActivity.this.finish();
                } else {
                    result.notImplemented();
                }
            }
        });
    }
}

      我们分析 FlutterFragmentFlutterActivity 时会发现,依旧是一层层封装的 FlutterView

      小菜测试 onMethodCall 中若有与 Flutter 中传递的相同 method name 时可以尝试获取传递参数;若此时需要向 Flutter 返回传递参数可以通过 result.success() 方法进行数据传递,若无需传递则可不设置当前方法;

      小菜理解,MethodChannel 主要是由 Flutter 主动向 Android 原生发起交互请求,小菜理解相对于于原生为被动式交互较多;

EventChannel

      EventChannel 可以由 Android 原生主动向 Flutter 发起交互请求,小菜理解相对于原生为主动式交互,类似于 Android 发送一个广播在 Flutter 端进行接收;其使用方式与 MethodChannel 类似,根据 FlutterView 进行绑定监听,与上述相似,小菜不分开写了;

      EventChannel 是对 Stream 流的监听,通过 onListener 进行消息发送,通过 onCancel 对消息取消;

new EventChannel(flutterView, CHANNEL).setStreamHandler(new EventChannel.StreamHandler() {
    @Override
    public void onListen(Object arguments, final EventChannel.EventSink events) {
        events.success("我来自 " + TAG +" !! 使用的是 EventChannel 方式");
    }

    @Override
    public void onCancel(Object arguments) {
    }
});

      Flutter 端通过 receiveBroadcastStream 进行数据流监听;分析源码得知,其内部同样是通过 invokeMethod 方法进行发送;listen 方法中,onData 为必须参数用作收到 Android 端发送数据的回调;onError 为数据接收失败回调;onDone 为接收数据结束回调;

StreamSubscription<T> listen(void onData(T event),
      {Function onError, void onDone(), bool cancelOnError});
class _MyHomePageState extends State<MyHomePage> {
  static const eventChannel = const EventChannel('ace_demo_android_flutter');
  String _result = '';
  StreamSubscription _streamSubscription;

  @override
  void initState() {
    super.initState();
    _getEventResult();
  }

  @override
  void dispose() {
    super.dispose();
    if (_streamSubscription != null) {
      _streamSubscription.cancel();
    }
  }

  _getEventResult() async {
    try {
      _streamSubscription =
          eventChannel.receiveBroadcastStream().listen((data) {
        setState(() {
          _result = data;
        });
      });
    } on PlatformException catch (e) {
      setState(() {
        _result = "event get data err: '${e.message}'.";
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(title: Text(widget.title)),
        body: Center(
            child: Text('${_result}',
                style: TextStyle(color: Colors.blueAccent, fontSize: 18.0))),
        floatingActionButton: FloatingActionButton(
            onPressed: _incrementCounter, child: Icon(Icons.arrow_back)));
  }
}

BasicMessageChannel

      BasicMessageChannel 主要传递字符串和半结构化的数据交互;其编解码有多种类型,在使用时建议 AndroidFlutter 两端一致;

  1. BinaryCodec:基本二进制编码类型;
  2. StringCodec:字符串与二进制之间的编码类型;
  3. JSONMessageCodecJson 与二进制之间的编码类型;
  4. StandardMessageCodec:默认编码类型,包括基础数据类型、二进制数据、列表、字典等与二进制之间等编码类型;

Flutter -> Android

      Flutter 端向 Android 端发送 send 数据请求,Android 端接收到后通过 replayFlutter 端发送消息,从而完成一次消息交互;

// Flutter 端
static const basicChannel = BasicMessageChannel<String>('ace_demo_android_flutter', StringCodec());

@override
void initState() {
  super.initState();
  _getBasicResult();
}
  
_getBasicResult() async {
  final String reply = await basicChannel.send('ace_demo_user');
  setState(() {
    _result = reply;
  });
}

// Android 端
final BasicMessageChannel channel = new BasicMessageChannel<String> (flutterView, CHANNEL, StringCodec.INSTANCE);
channel.setMessageHandler(new BasicMessageChannel.MessageHandler() {
    @Override
    public void onMessage(Object o, BasicMessageChannel.Reply reply) {
        reply.reply("我来自 " + TAG +" !! 使用的是 BasicMessageChannel 方式");
    }
});

Android -> Flutter

      根据上述继续由 Android 端主动向 Flutter 端发送数据,Android 通过 sendFlutter 发送数据请求,Flutter 通过 setMessageHandler 接收后向 Androidreturn 返回结果,再由 Android 回调接收,从而完成一次数据交互;

public void send(T message) {
    this.send(message, (BasicMessageChannel.Reply)null);
}

public void send(T message, BasicMessageChannel.Reply<T> callback) {
    this.messenger.send(this.name, this.codec.encodeMessage(message), callback == null ? null : new BasicMessageChannel.IncomingReplyHandler(callback));
}

      分析源码 send 有两个构造函数,有两个参数的构造方法用来接收 Flutter 回调的数据;

// Flutter 端
static const basicChannel = BasicMessageChannel<String>('ace_demo_android_flutter', StringCodec());

@override
void initState() {
  super.initState();
  _getBasicResult();
}

_getBasicResult() async {
  final String reply = await
  channel.setMessageHandler((String message) async {
    print('Flutter Received: ${message}');
    setState(() {
      _result = message;
    });
    return "{'name': '我不是老猪', 'gender': 1}";
  });
}

// Android 端
channel.setMessageHandler(new BasicMessageChannel.MessageHandler() {
    @Override
    public void onMessage(Object o, BasicMessageChannel.Reply reply) {
        reply.reply("我来自 " + TAG +" !! 使用的是 BasicMessageChannel 方式");
        channel.send("ace_demo_user");
        //channel.send("ace_demo_user", new BasicMessageChannel.Reply() {
        //    @Override
        //    public void reply(Object o) {
        //        Intent intent = new Intent();
        //        intent.putExtra("data", o!=null?o.toString():"");
        //        setResult(REQUEST_CODE, intent);
        //        MyFlutterViewActivity.this.finish();
        //    }
        //});
    }
});

注意事项

1. ensureInitializationComplete must be called after startInitialization

      小菜在从 AndroidFlutter 交互过程时,崩溃提示如下问题;

      小菜发现在 Application 中需要使用 FlutterApplicationFlutterApplication 的作用就是通过调用 FlutterMainstartInitialization 方法进行初始化;

import io.flutter.app.FlutterApplication;

public class MyApplication extends FlutterApplication {
}
2. 注意交互返回中内容是否为空

      小菜在测试 MethodChannel 时,invokeMethod 时尝试了一个参数和两个参数的构造,只有一个参数的 invokeMethod 是没有回调内容的,而小菜在 Android 端未判空,虽然没有报异常,但是后面的代码都没有执行,很基本的问题却困扰小菜很久,希望大家可以避免;

3. 多种 Platform Channel 共同使用

      小菜测试过程中,多种 Platform Channel 可以共同使用,可以根据业务或场景的不同配合使用,提高效率;


      小菜对 AndroidFlutter 交互还不够深入,仍需进一步学习;如有错误请多多指导!

来源:阿策小和尚
欢迎闲来吐槽~

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

推荐阅读更多精彩内容