Flutter如何与Native(Android)进行交互
前言
上一篇文章《Flutter混合开发:Android中如何启动Flutter》中我们介绍了如何在Native(Android项目)中启动Flutter,展示Flutter页面。但是在开发过程中,很多时候并不是简单的展示一个页面即可,还会涉及到各种交互,比如传递一些消息。
本篇文章就简单介绍一下Flutter与原生Native的三种交互方式:
BasicMessageChannel、MethodChannel和EventChannel。
BasicMessageChannel
虽然说是三种交互方式,但是其实本质都是一种,这个我们后面会解释。
先来看看BasicMessageChannel。它可以实现双方交互,发送一些简单消息,消息类型Object,但是并不是所有Object都可以,基础类型及基础类型的数组、list、map是可以的。这个可以参考BasicMessageChannel的源码:
public void send(@Nullable T message, @Nullable final Reply<T> callback) {messenger.send(name,codec.encodeMessage(message),callback == null ? null : new IncomingReplyHandler(callback));}
可以看到进行了encode,这个codec一般是StandardMessageCodec,它的encodeMessage函数源码:
public ByteBuffer encodeMessage(Object message) {if (message == null) {return null;}final ExposedByteArrayOutputStream stream = new ExposedByteArrayOutputStream();writeValue(stream, message);final ByteBuffer buffer = ByteBuffer.allocateDirect(stream.size());buffer.put(stream.buffer(), 0, stream.size());return buffer;}
这里writeValue的源码:
protected void writeValue(ByteArrayOutputStream stream, Object value) {if (value == null || value.equals(null)) {stream.write(NULL);} else if (value == Boolean.TRUE) {stream.write(TRUE);} else if (value == Boolean.FALSE) {stream.write(FALSE);} else if (value instanceof Number) {if (value instanceof Integer || value instanceof Short || value instanceof Byte) {stream.write(INT);writeInt(stream, ((Number) value).intValue());} else if (value instanceof Long) {stream.write(LONG);writeLong(stream, (long) value);} else if (value instanceof Float || value instanceof Double) {stream.write(DOUBLE);writeAlignment(stream, 8);writeDouble(stream, ((Number) value).doubleValue());} else if (value instanceof BigInteger) {stream.write(BIGINT);writeBytes(stream, ((BigInteger) value).toString(16).getBytes(UTF8));} else {throw new IllegalArgumentException("Unsupported Number type: " + value.getClass());}} else if (value instanceof String) {stream.write(STRING);writeBytes(stream, ((String) value).getBytes(UTF8));} else if (value instanceof byte[]) {stream.write(BYTE_ARRAY);writeBytes(stream, (byte[]) value);} else if (value instanceof int[]) {stream.write(INT_ARRAY);final int[] array = (int[]) value;writeSize(stream, array.length);writeAlignment(stream, 4);for (final int n : array) {writeInt(stream, n);}} else if (value instanceof long[]) {stream.write(LONG_ARRAY);final long[] array = (long[]) value;writeSize(stream, array.length);writeAlignment(stream, 8);for (final long n : array) {writeLong(stream, n);}} else if (value instanceof double[]) {stream.write(DOUBLE_ARRAY);final double[] array = (double[]) value;writeSize(stream, array.length);writeAlignment(stream, 8);for (final double d : array) {writeDouble(stream, d);}} else if (value instanceof List) {stream.write(LIST);final List<?> list = (List) value;writeSize(stream, list.size());for (final Object o : list) {writeValue(stream, o);}} else if (value instanceof Map) {stream.write(MAP);final Map<?, ?> map = (Map) value;writeSize(stream, map.size());for (final Entry<?, ?> entry : map.entrySet()) {writeValue(stream, entry.getKey());writeValue(stream, entry.getValue());}} else {throw new IllegalArgumentException("Unsupported value: " + value);}}
下面看一下如何来使用它,以Android端为例。
Android端
(1)不使用engine cache预热
如果不使用engine cache,那么在FlutterActivity的继承类中重写configureFlutterEngine:
class MainActivity : FlutterActivity() {var channel : BasicMessageChannel? = nulloverride fun configureFlutterEngine(flutterEngine: FlutterEngine) {super.configureFlutterEngine(flutterEngine)var channel = BasicMessageChannel<String>(flutterEngine.dartExecutor.binaryMessenger,"test" ,StringCodec.INSTANCE)channel.setMessageHandler { message, reply ->Log.e("recieve", message)}}
}
注意这里第二个参数"test"是这通道(channel)的名称,两边名称一致才能进行通信。
第三个参数是消息的编解码器,这里我们因为是简单的示例,消息是字符串String,所以用StringCodec。
StringCodec是MessageCodec接口的实现,除了它还有BinaryCodec,JsonMessageCodec,StandardMessageCodec。另外我们还可以自己实现MessageCodec,实现它的两个函数即可,它的源码如下:
public interface MessageCodec<T> {/*** Encodes the specified message into binary.** @param message the T message, possibly null.* @return a ByteBuffer containing the encoding between position 0 and the current position, or* null, if message is null.*/@NullableByteBuffer encodeMessage(@Nullable T message);/*** Decodes the specified message from binary.** @param message the {@link ByteBuffer} message, possibly null.* @return a T value representation of the bytes between the given buffer's current position and* its limit, or null, if message is null.*/@NullableT decodeMessage(@Nullable ByteBuffer message);
}
最后,MessageHandler用于接受从Flutter传递过来的消息。这里简单的将消息打印出来。
当需要向flutter发送消息时,执行channel?.send("android call")即可
(2)使用engine cache预热
一般情况我们在Application中添加cache,如下:
class App : Application() {companion object{...lateinit var flutterEngine2 : FlutterEngine}override fun onCreate() {super.onCreate()...flutterEngine2 = FlutterEngine(this)flutterEngine2.navigationChannel.setInitialRoute("second")flutterEngine2.dartExecutor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault())FlutterEngineCache.getInstance().put("second", flutterEngine2)}
}
这里我们为second这个flutter页面创建engine并加入cache进行预热。
如果我们想使用这个engine发送消息,那么可以直接创建BasicMessageChannel
var channel = BasicMessageChannel<String>(App.flutterEngine2.dartExecutor.binaryMessenger,"test" ,StandardMessageCodec.INSTANCE as MessageCodec<String>)
channel.setMessageHandler { message, reply ->Log.e("recieve", message)
}
后续与上面就一样了。
Flutter端
步骤基本一样,先创建
static const messageChannel = const BasicMessageChannel("test", StringCodec());
这里通道名称保持与native一致。
设置回调:
messageChannel.setMessageHandler((message) async{print(message)});
发送消息
messageChannel.send("flutter call");
这样就实现了Native和Flutter的双向消息交互。
MethodChannel
用于双方函数的调用,使用方法与BasicMessageChannel相似,其实本质上是一样的。我们先来看看如何使用它。
Android端
与BasicMessageChannel一样预热和不预热可以有两种不同的处理,但是其实最终都是获取到FlutterEngine对象,所以就不赘述了,直接使用即可。代码如下:
//创建var channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger,"test")//回调,根据call执行native函数channel.setMethodCallHandler { call, result ->when(call.method){"flutterCall" -> {//执行我们自定义的对应函数flutterCall(call.arguments)}else -> {}}}
这里flutterCall是响应Flutter发送过来的请求,我们定义一个对应的函数来处理,如:
fun flutterCall(arguments : Object){Log.e("flutterCall", "message:" + arguments.toString())}
然后我们可以通过invokeMethod函数来执行Flutter函数,如:
//执行flutter函数channel.invokeMethod("androidCall", "android message")
Flutter端
流程一样,代码如下:
//创建
static const methodChannel = const MethodChannel("test");
//回调,根据call执行flutter函数methodChannel.setMethodCallHandler((call) async {switch(call.method){case "androidCall"://执行自定义的对应函数androidCall(call.arguments);break;}});
//执行native函数
methodChannel.invokeMethod("flutterCall", "flutter message");
源码分析
在分析BasicMessageChannel时我们知道它的send函数其实是调用了messenger.send(...),这个messenger是BinaryMessenger,就是构造函数的第一个参数。MethodCannel也是一样,它的invokeMethod函数源码如下:
@UiThreadpublic void invokeMethod(String method, @Nullable Object arguments, @Nullable Result callback) {messenger.send(name,codec.encodeMethodCall(new MethodCall(method, arguments)),callback == null ? null : new IncomingResultHandler(callback));}
可以看到,最终还是调用了BinaryMessenger的send函数。只不过将invokeMethod的两个参数(String类型的函数名method和Object类型的参数arguments)封装到MethodCall中。
再来看回调的处理,上面invokeMethod函数中可以看到,用IncomingResultHandler将callback进行了封装,它的关键源码如下:
private final class IncomingMethodCallHandler implements BinaryMessageHandler {private final MethodCallHandler handler;IncomingMethodCallHandler(MethodCallHandler handler) {this.handler = handler;}@Override@UiThreadpublic void onMessage(ByteBuffer message, final BinaryReply reply) {final MethodCall call = codec.decodeMethodCall(message);try {handler.onMethodCall(call,new Result() {...});} catch (RuntimeException e) {...}}...}
可以看到在收到消息onMessage后先将消息解析成MethodCall在执行callback,这样就可以直接获取到函数名及参数了。
通过上面我们知道MethodChannel和BasicMessageChannel本质是一样的,只不过经过了一层MethodCall的封装,方便直接获取函数名和参数。
EventChannel
EventChannel与上面两个都不太一样,它是flutter发起,native处理并返回结果,flutter再处理结果。说它是单方向通道也不是很准确,但是native无法主动发起,所以更像是一个c/s结构。
先来看看如何使用。
Android端
同样需要FlutterEngine对象,代码如下:
//创建
var channel = EventChannel(flutterEngine.dartExecutor.binaryMessenger,"test")
//设置处理handler
channel.setStreamHandler(object : StreamHandler(), EventChannel.StreamHandler {override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {//根据arguments处理arguments?.let {...//将处理结果返回,可能成功也可能失败events?.success("android back")//events?.error("errorcode", "errormssage", null)//如果不返回,即success和error都不执行,则需要执行endOfStream//events?.endOfStream()}}override fun onCancel(arguments: Any?) {//执行取消操作}
})
上面提到Native无法主动发起,所以就没有类似上面send或invokeMethod函数。
Flutter端
通过receiveBroadcastStream来发送event请求,并通过linsten来监听返回。
//创建
static const eventChannel = const EventChannel("test");
//发送arguments给native处理,并监听结果
eventChannel.receiveBroadcastStream(["flutter event"]).listen((event) {//返回成功结果,处理print(event.toString());
}, onError: (event){//返回错误结果,处理
}, onDone: (){//执行完成处理
});
源码分析
我们来看一下receiveBroadcastStream的关键源码:
Stream<dynamic> receiveBroadcastStream([ dynamic arguments ]) {final MethodChannel methodChannel = MethodChannel(name, codec);late StreamController<dynamic> controller;controller = StreamController<dynamic>.broadcast(onListen: () async {binaryMessenger.setMessageHandler(name, (ByteData? reply) async {...});try {await methodChannel.invokeMethod<void>('listen', arguments);} catch (exception, stack) {...}}, onCancel: () async {binaryMessenger.setMessageHandler(name, null);try {await methodChannel.invokeMethod<void>('cancel', arguments);} catch (exception, stack) {...}});return controller.stream;}
可以看到EventChannel本质上就是MethodChannel,只不过执行了几个预先定义好的函数,如listen和cancel。这样对MethodChannel进行再次封装,可以更简单的进行事件传递。
总结
上面我们展示了三种交互方式的使用,并解析了其内部的联系。其实可以看到三种方式最终其实都是使用了BinaryMessenger这一抽象类的默认实现_DefaultBinaryMessenger。所以如果我们通过BinaryMessenger来实现一套自己特别的消息传递机制。
相关文章:
Flutter如何与Native(Android)进行交互
前言 上一篇文章《Flutter混合开发:Android中如何启动Flutter》中我们介绍了如何在Native(Android项目)中启动Flutter,展示Flutter页面。但是在开发过程中,很多时候并不是简单的展示一个页面即可,还会涉及…...
数据库主从复制和读写分离
主从数据库和数据库集群的一些问题 数据库集群和主从数据库最本质的区别,其实也就是data-sharing和nothing-sharing的区别。集群是共享存储的。主从复制中没有任何共享。每台机器都是独立且完整的系统。 什么是主从复制? 主从复制,是用来建立一个和主数…...
Java并发编程面试题——线程安全(原子性、可见性、有序性)
文章目录一、原子性高频问题1.1 Java中如何实现线程安全?1.2 CAS底层实现1.3 CAS的常见问题1.4 四种引用类型 ThreadLocal的问题?二、可见性高频问题2.1 Java的内存模型2.2 保证可见性的方式2.3 volatile修饰引用数据类型2.4 有了MESI协议,为啥还有vol…...
DialogFragment内存泄露问题能不能一次性改好
孽缘 自DialogFragment在Android3.0之后作为一种特殊的Fragment引入,官方建议使用DialogFragment代替Dialog或者AllertDialog来实现弹框的功能,因为它可以更好的管理Dialog的生命周期以及可以更好复用。 然而建议虽好,实用须谨慎,…...
java学习--多线程
多线程 了解多线程 多线程是指从软件或者硬件上实现多个线程并发执行的技术。 具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。 并发和并行 并行:在同一时刻,有多个指令在CPU上同时执行并发࿱…...
90后阿里P7技术专家晒出工资单:狠补了这个,真香...
最近一哥们跟我聊天装逼,说他最近从阿里跳槽了,我问他跳出来拿了多少?哥们表示很得意,说跳槽到新公司一个月后发了工资,月入5万多,表示很满足!这样的高薪资着实让人羡慕,我猜这是税后…...
2023美赛C题:Wordle筛选算法
Wordle 规则介绍 Wordle 每天会更新一个5个字母的单词,在6次尝试中猜出单词就算成功。每个猜测必须是一个有效的单词(不能是不能组成单词的字母排列)。 每次猜测后,字母块的颜色会改变,颜色含义如下: 程…...
SpringBoot 集成 Kafka
SpringBoot 集成 Kafka1 安装 Kafka2 创建 Topic3 Java 创建 Topic4 SpringBoot 项目4.1 pom.xml4.2 application.yml4.3 KafkaApplication.java4.4 CustomizePartitioner.java4.5 KafkaInitialConfig.java4.6 SendMessageController.java5 测试1 安装 Kafka Docker 安装 Kafk…...
OpenCV 图像金字塔算子
本文是OpenCV图像视觉入门之路的第14篇文章,本文详细的介绍了图像金字塔算子的各种操作,例如:高斯金字塔算子 、拉普拉斯金字塔算子等操作。 高斯金字塔中的较高级别(低分辨率)是通过先用高斯核对图像进行卷积再删除偶…...
【自学Linux】Linux一切皆文件
Linux一切皆文件 Linux一切皆文件教程 Linux 中所有内容都是以文件的形式保存和管理的,即一切皆文件,普通文件是文件,目录是文件,硬件设备(键盘、监视器、硬盘、打印机)是文件,就连套接字&…...
CUDA C++扩展的详细描述
CUDA C扩展的详细描述 文章目录CUDA C扩展的详细描述CUDA函数执行空间说明符B.1.1 \_\_global\_\_B.1.2 \_\_device\_\_B.1.3 \_\_host\_\_B.1.4 Undefined behaviorB.1.5 __noinline__ and __forceinline__B.2 Variable Memory Space SpecifiersB.2.1 \_\_device\_\_B.2.2. \_…...
为什么重写equals必须重写hashCode
关于这个问题,看了网上很多答案,感觉都参差不齐,没有答到要点,这次就记录一下! 首先我们为什么要重写equals?这个方法是用来干嘛的? public boolean equals (Object object&#x…...
< 每日小技巧:N个很棒的 Vue 开发技巧, 持续记录ing >
每日小技巧:6 个很棒的 Vue 开发技巧👉 ① Watch 妙用> watch的高级使用> 一个监听器触发多个方法> watch 监听多个变量👉 ② 自定义事件 $emit() 和 事件参数 $event👉 ③ 监听组件生命周期常规写法hook写法ὄ…...
数据结构与算法之二分查找分而治之思想
决定我们成为什么样人的,不是我们的能力,而是我们的选择。——《哈利波特与密室》二分查找是查找算法里面是很优秀的一个算法,特别是在有序的数组中,这种算法思想体现的淋漓尽致。一.题目描述及其要求请实现无重复数字的升序数组的…...
训练自己的中文word2vec(词向量)--skip-gram方法
训练自己的中文word2vec(词向量)–skip-gram方法 什么是词向量 将单词映射/嵌入(Embedding)到一个新的空间,形成词向量,以此来表示词的语义信息,在这个新的空间中,语义相同的单…...
ubuntu系统环境配置和常用软件安装
系统环境 修改文件夹名称为英文 参考链接 export LANGen_US xdg-user-dirs-gtk-update 常用软件安装 常用工具 ping 和ifconfig工具 sudo apt install -y net-tools inetutils-ping 截图软件 sudo apt install -y net-tools inetutils-ping flameshot 录屏 sudo apt-get i…...
【1139. 最大的以 1 为边界的正方形】
来源:力扣(LeetCode) 描述: 给你一个由若干 0 和 1 组成的二维网格 grid,请你找出边界全部由 1 组成的最大 正方形 子网格,并返回该子网格中的元素数量。如果不存在,则返回 0。 示例 1&#…...
windows11安装sqlserver2022报错
window11安装SQL Server 2022 报错 糟糕… 无法安装SQL Server (setup.exe)。此 SQL Server安装程序介质不支持此OS的语言,或没有SQL Server英语版本的安装文件。请使用匹配的特定语言SQL Server介质;或安装两个特定语言MUI,然后通过控制面板的区域设置…...
Python快速上手系列--日志模块--详解篇
前言本篇主要说说日志模块,在写自动化测试框架的时候我们就需要用到这个模块了,方便我们快速的定位错误,了解软件的运行情况,更加顺畅的调试程序。为什么要用到日志模块,直接print不就好了!那得写多少print…...
【THREE.JS学习(1)】绘制一个可以旋转、放缩的立方体
学习新技能,做一下笔记。在使用ThreeJS的时候,首先创建一个场景const scene new THREE.Scene();接着,创建一个相机其中,THREE.PerspectiveCamera()四个参数分别为:1.fov 相机视锥体竖直方向视野…...
装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...
iOS 26 携众系统重磅更新,但“苹果智能”仍与国行无缘
美国西海岸的夏天,再次被苹果点燃。一年一度的全球开发者大会 WWDC25 如期而至,这不仅是开发者的盛宴,更是全球数亿苹果用户翘首以盼的科技春晚。今年,苹果依旧为我们带来了全家桶式的系统更新,包括 iOS 26、iPadOS 26…...
【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
为什么需要建设工程项目管理?工程项目管理有哪些亮点功能?
在建筑行业,项目管理的重要性不言而喻。随着工程规模的扩大、技术复杂度的提升,传统的管理模式已经难以满足现代工程的需求。过去,许多企业依赖手工记录、口头沟通和分散的信息管理,导致效率低下、成本失控、风险频发。例如&#…...
什么是库存周转?如何用进销存系统提高库存周转率?
你可能听说过这样一句话: “利润不是赚出来的,是管出来的。” 尤其是在制造业、批发零售、电商这类“货堆成山”的行业,很多企业看着销售不错,账上却没钱、利润也不见了,一翻库存才发现: 一堆卖不动的旧货…...
EtherNet/IP转DeviceNet协议网关详解
一,设备主要功能 疆鸿智能JH-DVN-EIP本产品是自主研发的一款EtherNet/IP从站功能的通讯网关。该产品主要功能是连接DeviceNet总线和EtherNet/IP网络,本网关连接到EtherNet/IP总线中做为从站使用,连接到DeviceNet总线中做为从站使用。 在自动…...
【C++从零实现Json-Rpc框架】第六弹 —— 服务端模块划分
一、项目背景回顾 前五弹完成了Json-Rpc协议解析、请求处理、客户端调用等基础模块搭建。 本弹重点聚焦于服务端的模块划分与架构设计,提升代码结构的可维护性与扩展性。 二、服务端模块设计目标 高内聚低耦合:各模块职责清晰,便于独立开发…...
Java线上CPU飙高问题排查全指南
一、引言 在Java应用的线上运行环境中,CPU飙高是一个常见且棘手的性能问题。当系统出现CPU飙高时,通常会导致应用响应缓慢,甚至服务不可用,严重影响用户体验和业务运行。因此,掌握一套科学有效的CPU飙高问题排查方法&…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
Spring是如何解决Bean的循环依赖:三级缓存机制
1、什么是 Bean 的循环依赖 在 Spring框架中,Bean 的循环依赖是指多个 Bean 之间互相持有对方引用,形成闭环依赖关系的现象。 多个 Bean 的依赖关系构成环形链路,例如: 双向依赖:Bean A 依赖 Bean B,同时 Bean B 也依赖 Bean A(A↔B)。链条循环: Bean A → Bean…...
