[译]Flutter Platform Channels(一)

版本所有,转载请注明出处。

原文地址
配套视频

本文仅供自己学习,公开是为了方便部分朋友共同学习,不喜欢勿喷。

"UI很漂亮。但是Flutter如何处理平台独立的API呢?"

Flutter邀请你用Dart语言开发你的移动应用,一套代码可以同时构建AndroidiOS。但是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团队选择了不同的方法。 它并没有做的太多,但它够简单,功能也多,完全掌握在你手中。

首先,FlutterAndroidiOS应用程序环境托管。应用程序的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');
});

上面的注释也适用于AndroidiOS上的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(UInt8ListInt32ListInt64ListFloat64List)和键不是字符串的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,但已经被重命名了,以避免在代码中将MessageChannelMethodChannel混淆。 由于更普遍适用,method channels保持较短的名称。

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

推荐阅读更多精彩内容