Flutter的UI很棒,但是它可以处理平台特定的API吗?
Flutter邀请您使用Dart编程语言编写移动应用,并为Android和iOS进行构建。但是Dart不能编译为Android的Dalvik字节码,也无法在iOS上使用Dart / Objective-C绑定。这意味着您编写的Dart代码无需直接访问iOS Cocoa Touch和Android SDK的特定于平台的API。
只要您只写Dart在屏幕上绘制像素,这就没什么大问题。 Flutter框架及其底层图形引擎非常有能力自行实现。如果您除了画像素以外所做的一切都是文件或网络I / O以及相关的业务逻辑,那也不是问题。 Dart语言,运行时和库就在那里。
但优秀的应用程序需要与原生平台进行更深层次的集成:
- 通知,应用生命周期,深层链接...
- 传感器,照相机,电池,地理位置,声音,连接性...
- 与其他应用共享信息,启动其他应用,...
- 持久的首选项,特殊文件夹,设备信息,...
该列表范围很广,并且似乎随每个平台发行版一起增长。
可以将所有这些平台API的访问权限放入Flutter框架本身。但这将使Flutter变得更大,并为它提供更多改变的理由。实际上,这可能会导致Flutter落后于最新的平台版本。或使用平台API的“最小公分母”包装不能令开发者满意。或以书面解释平台差异以难驾驭的抽象概念使新手感到困惑。或版本碎片和错误。
想一想,可能是以上所有情况。
Flutter团队选择了另一种方法。它并不能做很多事情,但是它简单,多功能并且完全在您手中。
首先,Flutter由Android或iOS应用托管内置在其中。应用程序的Flutter部分包装在特定于平台的标准组件中,例如Android上的View和iOS上的UIViewController。因此,尽管Flutter让您使用Dart编写应用程序,但您可以在主机应用程序中直接在特定于平台的API之上使用Java / Kotlin或Objective-C / Swift进行任意操作。
其次,Platform Channel提供了一种简单的机制,用于在您的Dart代码和主应用程序的特定于平台的代码之间进行通信。这意味着您可以在主应用程序代码中公开平台服务,并从Dart端调用它。或相反亦然。
第三,Plugin使创建以Java或Kotlin编写的Android实现和以Objective-C或Swift编写的iOS实现为后盾的Dart API成为可能,并将它们打包为Flutter / Android / iOS,使用平台将其三合为一渠道。这意味着您可以重用,共享和分发Flutter如何使用特定平台API。
本文是对Platform Channel的深入介绍。从Flutter的消息传递基础开始,我将介绍消息/方法/事件通道的概念,并讨论一些API设计注意事项。没有API清单,而是用于复制粘贴重用的简短代码示例。根据我作为Flutter团队成员对flutter / plugins GitHub存储库所做的贡献,提供了使用指南的简要列表。本文以其他资源列表结尾,包括指向DartDoc / JavaDoc / ObjcDoc参考API的链接。
Platform channels API
对于大多数用例,您可能会使Method Channel进行平台通信。但是,由于它们的许多属性都来自更简单的message channel以及底层的binary messaging基础,因此我将从此处开始。
基础: asynchronous, binary messaging
// Send a binary message from Dart to the platform.
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上,可以使用以下Kotlin代码将此类消息作为java.nio.ByteBuffer接收:
// Receive binary messages from Dart on Android.
// This code can be added to a FlutterActivity subclass, typically
// in 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上与此类似。
// Receive binary messages from Dart on iOS.
// This code can be added to a FlutterAppDelegate subclass,
// typically in 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的将来完成和两个平台回调的执行是必需的。
线程。在平台的主UI线程上已收到并必须发送消息和答复。在Dart中,每个Dart隔离区(即,每个Flutter视图)只有一个线程,因此对于在此处使用哪个线程没有混淆。
异常。在Dart或Android消息处理程序中引发的任何未捕获的异常都将被框架捕获并记录下来,并将空响应发送回发送方。记录在回复处理程序中引发的未捕获的异常。
处理程序生命周期。已注册的消息处理程序将与Flutter视图(意味着Dart隔离,Android FlutterView实例和iOS FlutterViewController)一起保留并保持活动状态。您可以通过取消注册处理程序来缩短其寿命:只需使用相同的频道名称设置一个null(或不同)的处理程序即可。
处理程序的唯一性。处理程序保存在以通道名称为键的哈希映射中,因此每个通道最多可以有一个处理程序。在接收端未注册任何消息处理程序的通道上发送的消息将使用空答复自动响应。
同步通信。平台通信仅在异步模式下可用。这样可以避免在线程之间进行阻塞调用以及可能引起的系统级问题(性能低下,出现死锁的风险)。在撰写本文时,尚不清楚在Flutter中是否确实需要同步通信,如果需要,以什么形式。
在二进制消息级别上,您需要担心字节序之类的微妙细节,以及如何使用字节表示更高级别的消息(例如字符串或映射)。每当您要发送消息或注册处理程序时,还需要指定正确的通道名称。使之更容易将我们带到平台渠道:
平台通道是一个对象,它将通道名称和编解码器组合在一起,用于将消息序列化/反序列化为二进制形式并返回。
Message channels: name + codec
假设您要发送和接收字符串消息,而不是字节缓冲区。可以使用Message Channel(一种简单的平台通道,由字符串编解码器构造)完成此操作。以下代码显示了如何在Dart,Android和iOS的两个方向上使用消息通道:
// 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")
}
通道名称仅在通道构造时指定。之后,无需重复通道名称即可完成发送消息或设置消息处理程序的调用。更重要的是,我们将其留给字符串编解码器类来处理如何将字节缓冲区解释为字符串,反之亦然。
可以肯定地说,这些是高贵的优势,但是您可能会同意BasicMessageChannel并不会做那么多。这是故意的。上面的Dart代码等效于二进制消息传递基础的以下用法
const codec = StringCodec();
// Send message to platform and receive reply.
final String reply = codec.decodeMessage(
await BinaryMessages.send(
'foo',
codec.encodeMessage('Hello, world'),
),
);
print(reply);
// Receive messages from platform and send replies.
BinaryMessages.setMessageHandler('foo', (ByteData message) async {
print('Received: ${codec.decodeMessage(message)}');
return codec.encodeMessage('Hi from Dart');
});
此备注也适用于消息通道的Android和iOS实现。不涉及:
- 消息通道委托给二进制消息传递层进行所有通信。
- 消息通道不会跟踪已注册的处理程序本身。
- 消息通道轻量且无状态。
- 使用相同的频道名称和编解码器创建的两个消息频道实例是等效的(并且会干扰彼此的通信)。
由于各种历史原因,Flutter框架定义了四种不同的消息编解码器:
StringCodec使用UTF-8编码字符串。如我们所见,带有此编解码器的消息通道在Dart中的类型为BasicMessageChannel<String>。BinaryCodec通过在字节缓冲区上实现一致映射,此编解码器使您可以在不需要编码/解码的情况下享受通道对象的便利。具有此编解码器的Dart消息通道的类型为BasicMessageChannel<ByteData>。JSONMessageCodec处理“类似于JSON”的值(字符串,数字,布尔值,null,此类值的列表以及此类值的字符串键映射)。Lists和Maps是异构的,可以嵌套。在编码期间,这些值将转换为JSON字符串,然后使用UTF-8转换为字节。 Dart消息通道的编解码器类型为BasicMessageChannel<dynamic>。StandardMessageCodec的比JSON编解码器稍微多一点可以处理更广泛的值,还支持同构数据缓冲区(UInt8List,Int32List,Int64List,Float64List)和具有非字符串键的映射。数字的处理与JSON不同,Dart int在平台上以32或64位带符号整数形式实现,具体取决于幅度-决不作为浮点数。将值编码为自定义,合理紧凑和可扩展的二进制格式。标准编解码器被设计为Flutter中通道通信的默认选择。对于JSON,使用标准编解码器构造的Dart消息通道的类型为BasicMessageChannel<dynamic>。
您可能已经猜到了,消息通道可与任何满足简单协定的消息编解码器实现一起使用。这使您可以根据需要插入自己的编解码器。您必须使用Dart,Java / Kotlin和Objective-C / Swift实现兼容的编码和解码。
编解码器的演变。每个消息编解码器都可以在Dart中使用,作为Flutter框架的一部分,也可以在两个平台上使用,也可以作为Flutter公开给Java / Kotlin或Objective-C / Swift代码的库的一部分。 Flutter仅将编解码器用于应用程序内通信,而不会将其用作持久性格式。这意味着消息的二进制形式可能会从Flutter的一个版本更改为下一个版本,而不会发出警告。当然,Dart,Android和iOS编解码器实现是一起发展的,以确保由发送方编码的内容可以在两个方向上被接收方成功解码。
空消息。任何消息编解码器都必须支持并保留空消息,因为这是对在接收方未注册任何消息处理程序的通道上发送的消息的默认回复
在Dart中静态键入消息。配置了标准消息编解码器的消息通道为消息和回复提供动态类型。您通常可以通过分配一个类型变量来明确表明您的类型期望值:
final String reply1 = await channel.send(msg1);
final int reply2 = await channel.send(msg2);
但是在处理涉及泛型类型参数的答复时有一个警告:
final List<String> reply3 = await channel.send(msg3); // Fails.
final List<dynamic> reply3 = await channel.send(msg3); // Works.
第一行在运行时失败,除非答复为null。标准消息编解码器是为异构列表和映射编写的。在Dart方面,它们具有运行时类型List 和Map ,并且Dart 2阻止将此类值分配给具有更特定类型参数的变量。这种情况类似于Dart JSON反序列化,它产生List<dynamic> 和Map<String, dynamic> JSON消息编解码器也是如此。
Future会让您陷入类似的麻烦:
Future<String> greet() => channel.send('hello, world'); // Fails.
Future<String> greet() async { // Works.
final String reply = await channel.send('hello, world');
return reply;
}
即使收到的答复是字符串,第一个方法在运行时也会失败。无论答复的类型如何,通道实现都会创建Future<dynamic> ,并且无法将此类对象分配给Future<String> 。
为什么在BasicMessageChannel中有“Basic”?消息通道似乎仅在相当受限的情况下使用,在这种情况下您需要在隐式上下文中传达某种形式的同类事件流。就像键盘事件一样。对于大多数平台渠道的应用程序,您不仅需要传递值,还需要传递您希望每个值发生什么,或者您希望接收者如何解码它。一种方法是让消息代表以值作为参数的方法调用。因此,您需要一种将消息中的方法名称与参数分开的标准方法。而且,您还需要一种区分成功和错误回复的标准方法。这就是渠道为您服务的方式。现在,BasicMessageChannel最初被命名为MessageChannel,但为了避免将MessageChannel与MethodChannel混淆,将其重命名。方法通道更通用,因此名称更短。
Method Channel
方法通道是平台通道设计用于在Dart和Java / Kotlin或Objective-C / Swift中调用命名的代码段。方法通道利用标准化的消息“信封”将方法名称和参数从发送者传递到接收者,并在关联的回复中区分成功结果和错误结果。信封和受支持的有效负载由单独的方法编解码器类定义,类似于消息通道如何使用消息编解码器。
这就是方法通道所做的全部工作:将通道名称与编解码器组合在一起。
特别是,没有在方法通道上假设收到消息后执行什么代码。即使该消息表示方法调用,您也不必调用方法。您可能只是切换方法名称并为每种情况执行几行代码。
注意。缺少对方法及其参数的隐式或自动绑定可能会使您失望。很好,失望可以带来很多成果。我想您可以使用注释处理和代码生成从头开始构建这样的解决方案,或者您可以重用现有RPC框架的某些部分。 Flutter是开源的,随时可以贡献!如果方法通道符合要求,则可以将其用作代码生成的目标。同时,它们在“手工模式”下非常有用。
Method Channel是Flutter团队应对的挑战,即为当时不存在的插件生态系统定义可用的通信API的挑战。我们希望插件作者可以立即开始使用这些东西,而无需大量样板或复杂的构建设置。我认为Method Channel的概念是一个不错的答案,但是如果它仍然是唯一的答案,我会感到惊讶。
这是在从Dart的简单使用Method Channel调用一些平台代码。该代码与bar相关,但是bar不是方法名称,但可以这么做。它所做的只是构造一个问候字符串并将其返回给调用者,因此我们可以在合理的前提下(平台调用不会失败)进行编码(我们将在下面进一步处理错误处理):
// Invocation of platform methods, simple case.
// Dart side.
const channel = MethodChannel('foo');
final String greeting = await channel.invokeMethod('bar', 'world');
print(greeting);
// Android side.
val channel = MethodChannel(flutterView, "foo")
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
else -> result.notImplemented()
}
}
// iOS side.
let channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as! String)")
default: result(FlutterMethodNotImplemented)
}
}
通过在switch构造中添加cases,我们可以轻松地将上述内容扩展为处理多种方法。 default子句处理调用未知方法的情况(最有可能是由于编程错误)。
上面的Dart代码等效于以下代码:
const codec = StandardMethodCodec();
final ByteData reply = await BinaryMessages.send(
'foo',
codec.encodeMethodCall(MethodCall('bar', 'world')),
);
if (reply == null)
throw MissingPluginException();
else
print(codec.decodeEnvelope(reply));
方法通道的Android和iOS实现是类似的,是对二进制消息传递基础的调用的精简包装。空答复用于表示“未实现”结果。这很方便地使接收端的行为与调用是否落入switch中的default语句无关,或者根本没有在通道中注册任何方法调用处理程序。
示例中的参数值是单个字符串world。但是默认方法编解码器(恰当地称为“标准方法编解码器”)在后台使用标准消息编解码器来编码有效载荷值。这意味着,前面介绍的“类似于JSON的通用”值都支持作为方法参数和(成功的)结果。特别是,异构列表支持多个参数,而异构映射则支持命名参数。默认参数值为null。一些例子:
await channel.invokeMethod('bar');
await channel.invokeMethod('bar', <dynamic>['world', 42, pi]);
await channel.invokeMethod('bar', <String, dynamic>{
name: 'world',
answer: 42,
math: pi,
}));
Flutter SDK包含两个方法编解码器:
- StandardMethodCodec默认情况下,它将有效负载值的编码委托给
StandardMessageCodec。因为后者是可扩展的,所以前者也是可扩展的。 - JSONMethodCodec它将有效负载值的编码委托给
JSONMessageCodec。
您可以使用任何方法编解码器(包括自定义编解码器)来配置Method Channel。为了完全理解实现编解码器所涉及的内容,让我们通过使用一个容易犯错的baz方法扩展上面的示例,来研究如何在方法通道API级别处理错误:
// Method calls with error handling.
// Dart side.
const channel = MethodChannel('foo');
// Invoke a platform method.
const name = 'bar'; // or 'baz', or 'unknown'
const value = 'world';
try {
print(await channel.invokeMethod(name, value));
} on PlatformException catch(e) {
print('$name failed: ${e.message}');
} on MissingPluginException {
print('$name not implemented');
}
// Receive method invocations from platform and return results.
channel.setMethodCallHandler((MethodCall call) async {
switch (call.method) {
case 'bar':
return 'Hello, ${call.arguments}';
case 'baz':
throw PlatformException(code: '400', message: 'This is bad');
default:
throw MissingPluginException();
}
});
// Android side.
val channel = MethodChannel(flutterView, "foo")
// Invoke a Dart method.
val name = "bar" // or "baz", or "unknown"
val value = "world"
channel.invokeMethod(name, value, object: MethodChannel.Result {
override fun success(result: Any?) {
Log.i("MSG", "$result")
}
override fun error(code: String?, msg: String?, details: Any?) {
Log.e("MSG", "$name failed: $msg")
}
override fun notImplemented() {
Log.e("MSG", "$name not implemented")
}
})
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> result.success("Hello, ${call.arguments}")
"baz" -> result.error("400", "This is bad", null)
else -> result.notImplemented()
}
}
// iOS side.
let channel = FlutterMethodChannel(
name: "foo", binaryMessenger: flutterView)
// Invoke a Dart method.
let name = "bar" // or "baz", or "unknown"
let value = "world"
channel.invokeMethod(name, arguments: value) {
(result: Any?) -> Void in
if let error = result as? FlutterError {
os_log("%@ failed: %@", type: .error, name, error.message!)
} else if FlutterMethodNotImplemented.isEqual(result) {
os_log("%@ not implemented", type: .error, name)
} else {
os_log("%@", type: .info, result as! NSObject)
}
}
// Receive method invocations from Dart and return results.
channel.setMethodCallHandler {
(call: FlutterMethodCall, result: FlutterResult) -> Void in
switch (call.method) {
case "bar": result("Hello, \(call.arguments as! String)")
case "baz": result(FlutterError(
code: "400", message: "This is bad", details: nil))
default: result(FlutterMethodNotImplemented)
}
错误是三元组(代码,消息,详细信息),其中代码和消息是字符串。该消息提醒开发者,代码也是如此。错误详细信息是一些自定义值,通常为null,仅受编解码器支持的值种类的约束。
例外情况。 Dart或Android方法调用处理程序中引发的任何未捕获的异常都会被通道实现捕获并记录下来,并且错误结果会返回给调用者。记录在结果处理程序中引发的未捕获的异常。
方法编解码器如何编码数据是一个实现细节,就像消息编解码器如何将消息转换为字节一样。例如,方法编解码器可能使用列表:方法调用可以编码为两个元素的列表[方法名称,参数];成功结果作为一个列表[结果];错误结果显示为三元组列表[代码,消息,详细信息]。然后,可以通过委派给至少支持列表,字符串和null的基础消息编解码器来简单地实现这种方法编解码器。方法调用参数,成功结果和错误详细信息将是该消息编解码器支持的任意值。
API区别上面的代码示例突出说明了方法通道在Dart,Android和iOS上交付结果的方式非常不同:
- 在Dart端,调用由返回future的方法处理。future以成功情况下的调用result,错误情况下用
PlatformException,未实现情况下使用MissingPluginException来完成。 - 在Android上,调用由带有回调参数的方法处理。回调接口定义了三种方法,取决于结果。客户端代码实现了回调接口,以定义成功,错误以及未实现时应该发生的情况。
- 在iOS上,调用类似地由采用回调参数的方法处理。但是在这里,回调是一个单参数函数,为它提供了FlutterError实例,FlutterMethodNotImplemented常量,或者在成功的情况下提供了调用结果。客户端代码根据需要为块提供了条件逻辑来处理不同的情况。
这些差异也反映在编写消息调用处理程序的方式上,作为对Flutter SDK方法通道实现所使用的编程语言(Dart,Java和Objective-C)样式的让步。重做Kotlin和Swift中的实现可能会消除一些差异,但是必须注意避免增加使用Java和Objective-C中的方法通道的难度。
Event Channel:streaming
在Dart端使用平台事件流的方法如下:
// Consuming events on the Dart side.
const channel = EventChannel('foo');
channel.receiveBroadcastStream().listen((dynamic event) {
print('Received event: $event');
}, onError: (dynamic error) {
print('Received error: ${error.message}');
});
下面的代码以Android上的传感器事件为例,展示了如何在平台端生成事件。主要关注的问题是确保我们正在侦听来自平台源(在此情况下为传感器管理器)的事件,并在以下情况下通过事件通道发送事件:1)Dart端至少有一个流侦听器,以及2)环境活动正在运行。将必要的逻辑打包到一个类中可以增加正确执行此操作的机会:
// Producing sensor events on Android.
// SensorEventListener/EventChannel adapter.
class SensorListener(private val sensorManager: SensorManager) :
EventChannel.StreamHandler, SensorEventListener {
private var eventSink: EventChannel.EventSink? = null
// EventChannel.StreamHandler methods
override fun onListen(
arguments: Any?, eventSink: EventChannel.EventSink?) {
this.eventSink = eventSink
registerIfActive()
}
override fun onCancel(arguments: Any?) {
unregisterIfActive()
eventSink = null
}
// SensorEventListener methods.
override fun onSensorChanged(event: SensorEvent) {
eventSink?.success(event.values)
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
if (accuracy == SensorManager.SENSOR_STATUS_ACCURACY_LOW)
eventSink?.error("SENSOR", "Low accuracy detected", null)
}
// Lifecycle methods.
fun registerIfActive() {
if (eventSink == null) return
sensorManager.registerListener(
this,
sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE),
SensorManager.SENSOR_DELAY_NORMAL)
}
fun unregisterIfActive() {
if (eventSink == null) return
sensorManager.unregisterListener(this)
}
}
// Use of the above class in an Activity.
class MainActivity: FlutterActivity() {
var sensorListener: SensorListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
sensorListener = SensorListener(
getSystemService(Context.SENSOR_SERVICE) as SensorManager)
val channel = EventChannel(flutterView, "foo")
channel.setStreamHandler(sensorListener)
}
override fun onPause() {
sensorListener?.unregisterIfActive()
super.onPause()
}
override fun onResume() {
sensorListener?.registerIfActive()
super.onResume()
}
}
如果您在应用中使用android.arch.lifecycle包,则可以通过将其设置为LifecycleObserver来使SensorListener更加独立。
流处理程序的寿命。平台端流处理程序有两种方法onListen和onCancel,每当Dart流的侦听器数量分别从零变为1并返回时,就会调用这些方法。这可能会发生多次。流处理程序实现应该在调用前者时开始将事件倒入事件接收器,并在调用后者时停止。此外,当环境应用程序组件未运行时,它应该暂停。上面的代码提供了一个典型示例。在幕后,流处理程序当然只是二进制消息处理程序,使用事件通道的名称在Flutter视图中注册。
编解码器。事件通道配置有方法编解码器,从而使我们能够区分成功和错误事件,就像方法通道能够区分成功和错误结果一样。
流处理程序参数和错误。 onListen和onCancel流处理程序方法是通过方法通道调用来调用的。因此,我们有从Dart到平台的控制方法调用,以及具有相反方向的事件消息,它们都在同一逻辑通道上。此设置允许将参数传递给两种控制方法,并报告任何错误。在Dart端,参数(如果有的话)在对receiveBroadcastStream的调用中给出。这意味着它们仅被指定一次,无论在流的生存期内发生onListen和onCancel的调用次数如何。报告回的所有错误都将被记录。
流结束。事件接收器具有endOfStream方法,可以调用该方法来发出信号,表示不会发送其他成功或错误事件。空二进制消息用于此目的。在Dart端接收后,流将关闭。
流的生命周期。 Dart流由从传入的平台通道消息馈送的流控制器支持。使用事件通道的名称注册了二进制消息处理程序,以仅在流具有侦听器时才接收传入消息。
使用说明
通过域为channel添加前缀保证唯一性
channel名称只是字符串,但在用于您应用程序中用于不同目的的所有channel对象中,它们必须是唯一的。您可以使用任何合适的命名方案来实现。但是,对于插件中使用的通道,建议的方法是用一个域名称和插件名称来作为前缀例如some.body.example.com/sensors/foo,作为在sensor插件中使用的通道foo,由example.com的some.body开发。 这样做可以使插件使用者在其应用程序中组合任意数量的插件,而不会造成channel名称冲突的风险。
考虑将平台通道视为模块内通信
在分布式系统中调用远程过程调用的代码在表面上看起来类似于使用方法通道的代码:您调用由字符串给定的方法,并序列化参数和结果。由于分布式系统组件通常是独立开发和部署的,因此健壮的请求和答复检查至关重要,并且通常在网络两侧以检查和日志的方式进行。
另一方面,平台通道将三个一起开发和部署的代码粘合到一个组件中。
Java/Kotlin ↔ Dart ↔ Objective-C/Swift
实际上,将这样的三合会打包到单个代码模块(例如Flutter插件)中通常很有意义。这意味着跨方法通道调用进行参数和结果检查的需求应与跨同一模块内的常规方法调用进行这种检查的需求可比。
在模块内部,我们主要关心的是防止编程错误,这些错误超出了编译器的静态检查范围,并且在运行时未被发现,直到它们在时间或空间上将其局部炸毁为止。合理的编码风格是使用类型或断言使假设明确,从而使我们能够快速干净地失败,例如有一个例外。当然,具体内容因编程语言而异。例子:
- 如果期望通过平台通道接收的值具有某种类型,请立即将其分配给该类型的变量。
- 如果期望通过平台通道接收到的值是非空的,则可以将其设置为立即取消引用,或者断言该值是非空的,然后再存储以供以后使用。根据您的编程语言,您也许可以将其分配给非空类型的变量。
两个简单的例子:
// Dart: we expect to receive a non-null List of integers.
for (final int n in await channel.invokeMethod('getFib', 100)) {
print(n * n);
}
// Android: we expect non-null name and age arguments for
// asynchronous processing, delivered in a string-keyed map.
channel.setMethodCallHandler { call, result ->
when (call.method) {
"bar" -> {
val name : String = call.argument("name")
val age : Int = call.argument("age")
process(name, age, result)
}
else -> result.notImplemented()
}
}
:
fun process(name: String, age: Int, result: Result) { ... }
Android代码利用MethodCall的通用类型的 T参数(字符串键)方法,该方法在参数中查找键(假定为映射),并将找到的值强制转换为目标(调用站点)类型。如果由于某种原因失败,将引发一个适当的异常。从方法调用处理程序中抛出该异常后,它将被记录下来,并将错误结果发送到Dart端。
不要和platform channels冲突
在编写使用平台通道的Dart代码的单元测试时,可能会像在网络连接中那样模仿通道对象。
您当然可以这样做,但是实际上不需要模拟通道对象就可以在单元测试中很好地工作。相反,您可以注册模拟消息或方法处理程序,以在特定测试期间扮演平台的角色。这是功能hello的单元测试,应该在通道foo上调用bar方法:
test('gets greeting from platform', () async {
const channel = MethodChannel('foo');
channel.setMockMethodCallHandler((MethodCall call) async {
if (call.method == 'bar')
return 'Hello, ${call.arguments}';
throw MissingPluginException();
});
expect(await hello('world'), 'Platform says: Hello, world');
});
要测试设置消息或方法处理程序的代码,可以使用BinaryMessages.handlePlatformMessage合成传入的消息。目前,此方法尚未在平台通道上进行镜像,尽管可以轻松实现,如下面的代码所示。该代码定义了Hello类的单元测试,该类应该收集通道foo上对方法bar的调用的传入参数,同时返回问候语:
test('collects incoming arguments', () async {
const channel = MethodChannel('foo');
final hello = Hello();
final String result = await handleMockCall(
channel,
MethodCall('bar', 'world'),
);
expect(result, contains('Hello, world'));
expect(hello.collectedArguments, contains('world'));
});
// Could be made an instance method on class MethodChannel.
Future<dynamic> handleMockCall(
MethodChannel channel,
MethodCall call,
) async {
dynamic result;
await BinaryMessages.handlePlatformMessage(
channel.name,
channel.codec.encodeMethodCall(call),
(ByteData reply) {
if (reply == null)
throw MissingPluginException();
result = channel.codec.decodeEnvelope(reply);
},
);
return result;
}
上面的两个示例在单元测试中都声明了通道对象。除非您担心通道名称和编解码器重复,否则这可以正常工作,因为具有相同名称和编解码器的所有通道对象都是等效的。您可以通过将通道声明为生产代码和测试都可见的const来避免重复。
您不需要的是提供一种将模拟通道注入生产代码的方法。
考虑针对平台交互进行自动化测试
平台通道非常简单,但是通过自定义的Dart API从Flutter UI使所有工作正常进行,并由单独的Java / Kotlin和Objective-C / Swift实现作为后备,确实需要多加注意。在实践中,要保持设置在您的应用程序进行更改时正常工作,就需要进行自动测试以防止回归。仅凭单元测试无法做到这一点,因为您需要运行一个真正的应用程序来让平台通道与平台实际对话。
Flutter附带了flutter_driver集成测试框架,该框架可让您测试在真实设备和仿真器上运行的Flutter应用程序。但是flutter_driver当前未与其他框架集成,从而可以跨Flutter和平台组件进行测试。我相信这是Flutter未来将改进的领域。
在某些情况下,可以按原样使用flutter_driver来测试平台通道的使用情况。这要求您的Flutter用户界面可用于触发任何平台交互,然后对其进行足够详细的更新,以使您的测试能够确定交互的结果。
如果您不在那种情况下,或者将平台通道使用情况打包为需要模块测试的Flutter插件,则可以编写一个简单的Flutter应用进行测试。该应用应具有上述特征,然后可以使用flutter_driver进行练习。您可以在Flutter GitHub存储库中找到一个示例。
让平台端准备好接收同步呼叫
平台通道仅是异步的。但是,有很多平台API可以同步调用您的主机应用程序组件,以寻求信息或帮助,或者提供机会之窗。一个示例是Android上的Activity.onSaveInstanceState。保持同步意味着必须在呼入电话返回之前完成所有操作。现在,您可能希望在此类处理中包含来自Dart端的信息,但是一旦在主UI线程上已经激活了同步调用,就开始发送异步消息为时已晚。
Flutter使用的方法(尤其是用于语义/可访问性信息的方法)是,只要信息在Dart端发生更改,就主动将更新(或更新)信息发送到平台端。然后,当同步调用到达时,来自Dart端的信息已经存在并且可用于平台端代码。