[译]Flutter Platform Channels(一)
版本所有,转载请注明出处。
本文仅供自己学习,公开是为了方便部分朋友共同学习,不喜欢勿喷。
"UI很漂亮。但是Flutter如何处理平台独立的API呢?"
Flutter邀请你用Dart
语言开发你的移动应用,一套代码可以同时构建Android
和iOS
。但是Dart
不会编译成Android’s Dalvik字节码,在iOS
上也不会有Dart/Objective-C的绑定。这意味你的Dart
代码并不会直接访问平台特定的API,即 iOS Cocoa Touch 以及 Android SDK的API。
如果你只是通过Dart在屏幕上绘制像素并不会有太多部分。 Flutter框架及其底层图形引擎能足够的能力独立完成他们的工作。 如果除了绘制像素之外你所做的一切都是文件或网络I/O和相关的业务逻辑,那这也不是问题。Dart语言的运行时和库可以满足你的需求。
但是一些不平凡的应用需要和宿主平台有一个更深层次的集成:
- 通知, 应用生命周期, 深链接,...
- 传感器, 相机, 电池, 地理位置, 声音,网络连接,...
- 与其他应用共享数据,打开其他的应用,...
- 持久首选项,特殊文件夹,设备信息,...
对所有这些平台API的访问可以融入Flutter框架本身。 但这会使Flutter
体积变得更大,并给它更多的理由作出改变。 实际上,这可能会导致Flutter
落后于最新的平台版本。或者以“最小公分母"的原则来包装平台独立的API,这会使用程序开发者十分不爽。 或者用笨拙的抽象来解决平台差异,但这会使新手很困惑。 或者出现版本碎片, 或者产生Bug。
想一想,可能出现上面所有问题。
Flutter团队选择了不同的方法。 它并没有做的太多,但它够简单,功能也多,完全掌握在你手中。
首先,Flutter
由Android
或iOS
应用程序环境托管。应用程序的Flutter部分包含在标准的平台特定组件中,例如Android
上的View以及iOS
上的UIViewController。因此,虽然Flutter
邀请你在Dart
中编写app,但你依然可以在宿主app中使用Java/Kotlin或*Objective-C/Swift执行尽可能多的操作,直接调用平台特定的API。
其次,platform channels提供了一种简单的机制用来在Dart代码和宿主app的平台特定代码之间进行通信。这意味着你可以在宿主app代码中暴露平台服务,并从Dart
端调用它。反之亦然。
第三,插件可以创建由原生支持的Dart API,Android上可以用Java或者Kotlin实现,iOS上可以用Objective-C或者Swift实现。并且可以将其打包,从而实现Flutter/Android/iOS
三合一体。这意味着你可以重用,共享和分发。
本文是对平台渠道的深入介绍。 从Flutter
的消息传递基础开始,我将介绍消息/方法/事件( message/method/event )通道概念,并讨论一些API设计注意事项。 不会有API列表,而是用于复制粘贴重用的短代码示例。根据我作为Flutter团队成员对flutter/plugins做出贡献的经验,我会提供一份使用指南的简要列表。 本文最后列出了其他资源,包括DartDoc / JavaDoc / ObjcDoc参考API的链接。
概念列表
Platform channels API
基础:异步,二进制消息传递
消息通道:名称+编解码器
Method channels: 标准化的信封
Event channels: 流
使用指南
根据域为唯一性添加通道名称
考虑将platform channels视为模块内通信
不要模拟platform channels
考虑为您的平台交互自动化测试
保持平台端准备好接收同步调用
资源
Platform channels API
大部分情况下,你可能会使用method channels进行平台通信。 但由于它们的许多属性都来自更简单的消息通道和底层的二进制消息传递基础,所以我将从那里开始。
基础:异步,二进制消息传递
channels从最基本层面上来讲,Flutter
通过使用带有二进制消息的异步消息与平台代码进行通信 - 这意味着消息有效负载是一个byte buffer。 为了区分用于不同目的的消息,每个消息都在逻辑“channel”上发送,这个逻辑“channel”仅仅是一个带有名字的字符串。 以下例子使用了一个名称foo通道。
//向平台发送二进制消息.
final WriteBuffer buffer = WriteBuffer()
..putFloat64(3.1415)
..putInt32(12345678);
final ByteData message = buffer.done();
await BinaryMessages.send('foo', message);
print('Message sent, reply ignored');
在Android上,可以使用java.nio.ByteBuffer来接收该消息,以Kotlin为例:
// 在Android上接收来自Dart的二进制消息.
//此代码可以添加到FlutterActivity子类中,
// 通常是在onCreate中。
flutterView.setMessageHandler("foo") { message, reply ->
message.order(ByteOrder.nativeOrder())
val x = message.double
val n = message.int
Log.i("MSG", "Received: $x and $n")
reply.reply(null)
}
ByteBuffer API支持读取原始值,同时自动提前当前读取位置。 iOS上类似; 我并不擅长Swift,欢迎提出改进意见:
// 在os上接收来自Dart的二进制消息.
// 此代码可以添加到FlutterAppDelegate 子类中的
// 通常是在application:didFinishLaunchingWithOptions:中.
let flutterView =
window?.rootViewController as! FlutterViewController;
flutterView.setMessageHandlerOnChannel("foo") {
(message: Data!, reply: FlutterBinaryReply) -> Void in
let x : Float64 = message.subdata(in: 0..<8)
.withUnsafeBytes { $0.pointee }
let n : Int32 = message.subdata(in: 8..<12)
.withUnsafeBytes { $0.pointee }
os_log("Received %f and %d", x, n)
reply(nil)
}
通信是双向的,因此你也可以从相反的方向发送消息,从Java/Kotlin或Objective-C/Swift到Dart。 颠倒上述设置的方向如下:
// Send a binary message from Android.
val message = ByteBuffer.allocateDirect(12)
message.putDouble(3.1415)
message.putInt(123456789)
flutterView.send("foo", message) { _ ->
Log.i("MSG", "Message sent, reply ignored")
}
// Send a binary message from iOS.
var message = Data(capacity: 12)
var x : Float64 = 3.1415
var n : Int32 = 12345678
message.append(UnsafeBufferPointer(start: &x, count: 1))
message.append(UnsafeBufferPointer(start: &n, count: 1))
flutterView.send(onChannel: "foo", message: message) {(_) -> Void in
os_log("Message sent, reply ignored")
}
// Receive binary messages from the platform.
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
final ReadBuffer readBuffer = ReadBuffer(message);
final double x = readBuffer.getFloat64();
final int n = readBuffer.getInt32();
print('Received $x and $n');
return null;
});
要点。
强制性回复。 每个消息发送都涉及来自接收器的异步回复。 在上面的例子中,对于回传值并没有兴趣,但是空回复(null)对于Dart Future完成和两个平台回调的执行是必要的。
线程。 收到消息和回复,并且必须在平台的主UI线程上发送。 在Dart中,每个Dart isolate只有一个线程,即每个Flutter视图,因此不必对使用了哪个线程而感到困惑。
异常。 在Dart或Android消息处理程序中抛出的任何未捕获的异常都会被框架捕获并记录,并将null发送回发送方。在回复处理程序中抛出的未捕获异常也会被记录。
Handler的寿命(Handler lifetime)。 message handlers与Flutter View (意味着Dart隔离,Android FlutterView
实例和iOS FlutterViewController
实例)一起保留,一起存活。 你可以通过取消注册来缩短处理程序的生命周期:只需要将相同的Channel设置一个null或者不同的handler。
Handler唯一性。 Handlers被保存在由键为Channel名称的HashMap中,因此每个通道最多只能有一个Handler。 如果通过一个在接收端没有注册handler的channel发送消息,系统会自动使用null回复。
同步通信。 平台通信仅在异步模式下可用。 这样可以避免跨线程进行阻塞调用以及可能带来的系统级问题(性能低下,死锁风险)。 在撰写本文时,对于Flutter中是否真的需要同步通信并不完全清楚,如果真的需要,那么以何种形式存在也不完全清楚。
使用二进制消息,你需要考虑十分精细的细节,如字节序以及如何使用字节表示更高级别的消息,如字符串或映射。 每当要发送消息或注册handler时,还需要指定正确的通道名称。 这使得我们更想去使用message channels:
一个platform channel是一个对象,它将通道名称和编解码器组合在一起,用于将消息序列化/反序列化为二进制形式和返回。
Message channels: 名称+ 编解码器
message channels假设你要发送和接收字符串消息而不是字节缓冲区( byte buffers)。 这可以使用message channel完成,message channel是一种简单的平台通道,由字符串编解码器构成。 以下代码显示了如何在Dart,Android和iOS的两个方向上使用message channel:
// String messages
// Dart side
const channel = BasicMessageChannel<String>('foo', StringCodec());
// Send message to platform and receive reply.
final String reply = await channel.send('Hello, world');
print(reply);
// Receive messages from platform and send replies.
channel.setMessageHandler((String message) async {
print('Received: $message');
return 'Hi from Dart';
});
// Android side
val channel = BasicMessageChannel<String>(
flutterView, "foo", StringCodec.INSTANCE)
// Send message to Dart and receive reply.
channel.send("Hello, world") { reply ->
Log.i("MSG", reply)
}
// Receive messages from Dart and send replies.
channel.setMessageHandler { message, reply ->
Log.i("MSG", "Received: $message")
reply.reply("Hi from Android")
}
// iOS side
let channel = FlutterBasicMessageChannel(
name: "foo",
binaryMessenger: controller,
codec: FlutterStringCodec.sharedInstance())
// Send message to Dart and receive reply.
channel.sendMessage("Hello, world") {(reply: Any?) -> Void in
os_log("%@", type: .info, reply as! String)
}
// Receive messages from Dart and send replies.
channel.setMessageHandler {
(message: Any?, reply: FlutterReply) -> Void in
os_log("Received: %@", type: .info, message as! String)
reply("Hi from iOS")
}
channel的名称只能在构造channel时指定。 之后,我们不必在发传消息或者设置handler时指定channel名称。 更重要的是,我们将它留给字符串编解码器(String codec)来处理,字符串编解码器会将byte buffer转换成字符串,反之亦然。
这些优势很明显,但你可能会同意BasicMessageChannel
并没有做那么多。 这是故意的。 上面的Dart代码与下面使用二进制消息是等价:
const codec = StringCodec();
// 从平台发送消息并回复。
final String reply = codec.decodeMessage(
await BinaryMessages.send(
'foo',
codec.encodeMessage('Hello, world'),
),
);
print(reply);
// 从平台接收消息并回复.
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
print('Received: ${codec.decodeMessage(message)}');
return codec.encodeMessage('Hi from Dart');
});
上面的注释也适用于Android和iOS上的message channel。 并没有魔法:
- Message channels委托binary messaging层进行所有通信。
- Message channels 本身不跟踪已注册的handlers。
- Message channels是轻量级的,并且无状态。
- 如果两个Message channel的实例使用了相同的通道名称和编解码器是等价的(并且干扰彼此的通信)。
由于各种历史原因,Flutter定义了四种不同的消息编解码器:
-
StringCodec使用UTF-8对字符串进行编码。正如我们刚刚看到的,使用
StringCodec
的message channels 在Dart中的类型是BasicMessageChannel <String>
。 -
BinaryCodec在byte buffer级别上实现了身份映射,使用
BinaryCodec
允许你在不需要编码/解码的情况下享受通道对象的便利。使用BinaryCodec
的message channels 在Dart中的类型是BasicMessageChannel <ByteData>
。 -
JSONMessageCodec 是用来处理'Json-like'数据(字符串,数字,布尔值,null,元素为此类值的list以及键为字符串值为此类值的Map)进。List和Map是异构的,可以嵌套。在编码期间,这些值会被转换为JSON字符串,然后使用UTF-8转换为字节。 使用
JSONMessageCodec
的message channels 在Dart中的类型是BasicMessageChannel <dynamic>
。 -
StandardMessageCodec处理的数据要比JSON codec处理的数据稍微通用一些,支持同类数据缓冲区即buffer(
UInt8List
,Int32List
,Int64List
,Float64List
)和键不是字符串的map。数字的处理不同于JSON,Dart 的整型(int)在不同平台上表现有所不同,可能是32位也可能是64位的,这取于数据大小 - 但不会当作浮点数。数据会被编码成二进制格式,编码具有可自定义,合理而紧凑以及可扩展的特征。在flutter中,通道通信默认选用的是标准解码器(StandardMessageCodec)。就JSON而言,使用StandardMessageCodec
的message channels 在Dart中的类型是BasicMessageChannel <dynamic>
。
你可能已经猜到,message channels可以与任何实现了满足简单契约的消息编解码器一起使用。 如果有需要,你也可以插入自己的编解码器。 你必须在Dart,Java / Kotlin和Objective-C / Swift中实现兼容的编码和解码。
要点
编解码器演变。 每个消息编解码器都可以在Dart中使用,它是Flutter Framework的一部分,也可以在两个平台上使用,作为Flutter向Java / Kotlin或Objective-C / Swift代码公开的库的一部分。 Flutter仅将编解码器用于应用内部通信,而不是持久性格式。 这意味着消息的二进制形式可能会从一个Flutter版本更改为下一个版本,而不会发出警告。 当然,Dart,Android和iOS编解码器实现是一起演进的,以确保接收者可以成功解码由发送者发送的已被编码内容,这其中包括两个方向。
空(Null)消息。 任何消息编解码器都必须支持并保留空消息,因为如果在一个channel在接收方上没有注册handler的话,空消息将被用作默认回复消息。
在Dart中使用静态类型。 使用标准消息编解码器配置的message channel,无论是发送的消息还是回复都是dynamic
的。 你通常会通过分配类型变量来明确你期望的类型:
final String reply1 = await channel.send(msg1);
final int reply2 = await channel.send(msg2);
但是如果处理一个带有泛型参数的回复时就会遇到问题:
final List<String> reply3 = await channel.send(msg3); // 失败.
final List<dynamic> reply3 = await channel.send(msg3); // 好用.
第一行代码在运行时会遇到错误,除非回复为null。 标准消息编解码器是为异构list和map编写的。 在Dart方面,它们的运行时类型分别为List <dynamic>
和Map <dynamic,dynamic>
,而Dart 2会防止这样的值被赋给具有更多特定类型的参数。 这种情况类似于Dart JSON反序列化,Dart JSON反序列化会生成List <dynamic>
和Map <String,dynamic>
- 和JSON消息编解码器一样。
Futures会让你遇到类似的麻烦:
Future<String> greet() => channel.send('hello, world'); // 失败.
Future<String> greet() async { // 好用.
final String reply = await channel.send('hello, world');
return reply;
}
第一种方法在运行时会遇到错误,即使收到的回复是字符串。 无论回复的类型如何,通道的实现都会的类型为Future <dynamic>
的回复,并且无法将此这样的对象赋值给Future <String>
。
为什么BasicMessageChannel
中的有个“basic”? Message channels
似乎仅在相当受限的情况下使用,也就是说你要在隐含的上下文中传达某种形式的同类事件流。 或许像键盘事件一样。 对于使用了platform channel的大多数应用程序,你需要交流的不仅仅是值,也包括你希望每个值会生什么,或者你希望接收者如何解释这个值 。 一种方法是让消息表示一个方法调用,并将它的值作为参数。 因此,你需要一种将方法名称与消息中的参数分开的标准方法。 而且你还需要一种标准方法来区分成功回复和错误回复。 这些工作已经由method channel实现了。 现在,BasicMessageChannel
最初名为MessageChannel
,但已经被重命名了,以避免在代码中将MessageChannel
与MethodChannel
混淆。 由于更普遍适用,method channels保持较短的名称。