版本所有担敌,轉(zhuǎn)載請(qǐng)注明出處檀何。
本文僅供自己學(xué)習(xí)重虑,公開(kāi)是為了方便部分朋友共同學(xué)習(xí)践付,不喜歡勿噴。
Method channels:標(biāo)準(zhǔn)化信封
Method channels是platform channels的一種缺厉,用于調(diào)用Dart和Java / Kotlin或Objective-C / Swift中的命名代碼段荔仁。 方法通道利用標(biāo)準(zhǔn)化消息“信封”來(lái)傳遞從發(fā)送方到接收方的方法名稱和參數(shù),并區(qū)分相關(guān)答復(fù)中的成功和錯(cuò)誤結(jié)果芽死。 信封和支持的有效負(fù)載由單獨(dú)的方法編解碼器類定義,類似于message channels 如何使用消息編解碼器次洼。
Method channels所做的就是:將通道名稱與編解碼器組合在一起关贵。
特別地,對(duì)于在接收到Method channels上的消息時(shí)執(zhí)行什么代碼沒(méi)有做任何假設(shè)卖毁。 即使消息表示方法調(diào)用揖曾,你也不必調(diào)用方法。 你可以只打開(kāi)方法名稱并為每種情況執(zhí)行幾行代碼亥啦。
邊注炭剪。缺乏對(duì)方法及其參數(shù)的默示或自動(dòng)綁定可能會(huì)令你失望。 那很好翔脱,失望也能產(chǎn)生積極的影響奴拦。 我想你可以使用注釋處理和代碼器生成從頭開(kāi)始構(gòu)建這樣的解決方案,或者你可以重用現(xiàn)有RPC框架的一部分届吁。 Flutter是開(kāi)源的错妖,隨時(shí)貢獻(xiàn)绿鸣! 如果符合條件,Method channels可以使用代碼生成來(lái)實(shí)現(xiàn)暂氯。 同時(shí)潮模,它們?cè)凇笆止つJ健敝幸埠苡杏谩?/p>
Method channels是Flutter團(tuán)隊(duì)對(duì)定義可行通信API的挑戰(zhàn)的回答,以供當(dāng)時(shí)并不存在的插件生態(tài)系統(tǒng)使用痴施。 我們想要一些插件作者可以立即開(kāi)始使用的東西擎厢,而不需要很多樣板或復(fù)雜的構(gòu)建設(shè)置。 我認(rèn)為method channel的概念是一個(gè)不錯(cuò)的答案辣吃,但如果它仍然是唯一的答案动遭,我會(huì)感到驚訝。
下面演示是簡(jiǎn)單情況下你如何從Dart端使用method channel調(diào)用一些平臺(tái)代碼齿尽。 代碼與名稱bar相關(guān)聯(lián)沽损,在這種情況下,該名稱bar不是方法名稱循头,但可能是绵估。 它所做的就是構(gòu)造一個(gè)問(wèn)候語(yǔ)字符串并將其返回給調(diào)用者,因此我們可以在合理的假設(shè)下對(duì)平臺(tái)調(diào)用進(jìn)行編碼卡骂,這將不會(huì)失敼选(我們將在下面進(jìn)一步討論錯(cuò)誤處理):
// 簡(jiǎn)單情況下調(diào)用平臺(tái)方法
// 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)
}
}
通過(guò)向switch添加分支條件,我們可以輕松擴(kuò)展上述內(nèi)容以處理多種方法全跨。 default子句處理調(diào)用未知方法的情況(很可能是由于編程錯(cuò)誤)缝左。
上面的Dart代碼等效于以下內(nèi)容:
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));
MethodChannel在Android和iOS)上的實(shí)現(xiàn)同樣是對(duì)BinaryMessage的簡(jiǎn)單封裝。 空回復(fù)用來(lái)表示“未實(shí)現(xiàn)”浓若。 這使得接收者并不關(guān)心方法的調(diào)用在switch-case語(yǔ)句中是否出現(xiàn)貫穿到default中的現(xiàn)象渺杉,也不會(huì)關(guān)心根本沒(méi)有向通道注冊(cè)方法調(diào)用處理程序。
示例中的參數(shù)值是單個(gè)字符串string挪钓。 但是是越,默認(rèn)方法編解碼器,恰當(dāng)?shù)孛麨椤皊tandard method codec”碌上,使用standard message codec來(lái)編碼有效負(fù)載值倚评。 這意味著前面描述的“類JSON”值都支持作為方法參數(shù)和(成功)結(jié)果。 特別是馏予,異構(gòu)列表支持多個(gè)參數(shù)天梧,而異構(gòu)映射支持命名參數(shù)。 默認(rèn)參數(shù)值為null霞丧。 幾個(gè)例子:
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包含了兩種method codec:
-
[StandardMethodCodec](https://docs.flutter.io/flutter/services/StandardMethodCodec-class.html)
which by default delegates the encoding of payload values toStandardMessageCodec
. Because the latter is extensible, so is the former. -
[JSONMethodCodec](https://docs.flutter.io/flutter/services/JSONMethodCodec-class.html)
which delegates the encoding of payload values toJSONMessageCodec
.
- StandardMethodCodec呢岗,默認(rèn)情況下將有效負(fù)載值的編碼委托給StandardMessageCodec。 因?yàn)楹笳呤强蓴U(kuò)展的,前者也是如此敷燎。
- JSONMethodCodec 暂筝,它將有效負(fù)載值的編碼委托給JSONMessageCodec。
您可以使用任何方法編解碼器配置method channels硬贯,包括自定義編解碼器焕襟。 為了完全理解實(shí)現(xiàn)編解碼器所涉及的內(nèi)容,讓我們通過(guò)使用易錯(cuò)的baz方法擴(kuò)展上面的示例來(lái)查看如何在method channels API級(jí)別處理錯(cuò)誤:
// 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)
}
錯(cuò)誤是由三部分組成的(cod饭豹,消息鸵赖,詳細(xì)信息),其中code和消息是字符串拄衰。 message旨在供人使用它褪,code就是code。 錯(cuò)誤詳細(xì)信息是一些自定義值翘悉,通常為null茫打,受編解碼器支持的值類型的約束。
要點(diǎn)
異常妖混。 Dart或Android方法調(diào)用處理程序中拋出的任何未捕獲的異常都會(huì)被channel捕獲老赤,并記錄,并將錯(cuò)誤結(jié)果返回給調(diào)用者制市。 結(jié)果處理程序中拋出的未捕獲異常會(huì)被記錄抬旺。
信封編碼。 方法編解碼器如何對(duì)信封細(xì)節(jié)的編碼就像消息編解碼器如何將消息轉(zhuǎn)換為字節(jié)一樣祥楣。 例如开财,方法編解碼器可能使list:方法調(diào)用可以編碼為雙元素list[方法名稱,參數(shù)]; 成功結(jié)果作為單元素list[結(jié)果]; 錯(cuò)誤結(jié)果為三元素list[代碼误褪,消息责鳍,詳細(xì)信息]。 然后兽间,這種方法編解碼器可以簡(jiǎn)單地通過(guò)委托給支持至少list薇搁,字符串和null的基礎(chǔ)消息編解碼器來(lái)實(shí)現(xiàn)。 方法調(diào)用時(shí)的參數(shù)渡八,成功結(jié)果以及錯(cuò)誤詳細(xì)信息將是該消息編解碼器支持的任意值。
API differences. The code examples above highlight that method channels deliver results very differently across Dart, Android, and iOS:
API不同传货。 上面的代碼示例突出顯示method channels 在Dart屎鳍,Android和iOS上返回處理結(jié)果的方式很不一樣:
- 在Dart方面,調(diào)用由返回值為Future的方法處理问裕。 Future在成功的時(shí)候返回結(jié)果逮壁,發(fā)現(xiàn)錯(cuò)誤的時(shí)候會(huì)出現(xiàn)PlatformException,在沒(méi)有實(shí)現(xiàn)對(duì)應(yīng)方法的時(shí)候會(huì)拋出MissingPluginException異常粮宛。
- 在Android上窥淆,調(diào)用由參數(shù)為回調(diào)的方法處理卖宠。 回調(diào)接口定義了三種方法,根據(jù)結(jié)果調(diào)用其中的一種方法忧饭。 客戶端代碼實(shí)現(xiàn)回調(diào)接口扛伍,以定義成功,出錯(cuò)和未實(shí)現(xiàn)時(shí)應(yīng)該發(fā)生的事情词裤。
- 在iOS上刺洒,調(diào)用類似地由采用回調(diào)參數(shù)的方法處理。 但是在這里吼砂,回調(diào)是一個(gè)單參數(shù)函數(shù)逆航,它給出了FlutterError實(shí)例,F(xiàn)lutterMethodNotImplemented常量渔肩,或者因俐,如果成功,則給出調(diào)用的結(jié)果周偎。 客戶端代碼根據(jù)需要提供具有條件邏輯的塊以處理不同的情況抹剩。
These differences, mirrored also in the way message call handlers are written, arose as concessions to the styles of the programming languages (Dart, Java, and Objective-C) used for the Flutter SDK method channel implementations. Redoing the implementations in Kotlin and Swift might remove some of the differences, but care must be taken to avoid making it harder to use method channels from Java and Objective-C.
這些差異也反映在消息調(diào)用處理程序的編寫(xiě)方式中,這些差異是對(duì)在FlutterSDK中實(shí)現(xiàn)method channel的編程語(yǔ)言(Dart栏饮,Java和Objective-C)的讓步吧兔。 重做Kotlin和Swift中的實(shí)現(xiàn)可能會(huì)消除一些差異,但必須注意避免使用Java和Objective-C中的方法通道變得更加困難袍嬉。
Event channels: 流
event channel是一個(gè)專用平臺(tái)的通道境蔼,用于將平臺(tái)事件作為Dart流暴露給Flutter的用例。 Flutter SDK目前不支持將Dart流暴露給對(duì)應(yīng)該平臺(tái)代碼伺通,但如果需要箍土,可以構(gòu)建它。
以下是你在Dart端使用平臺(tái)事件流的方法:
// 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上的傳感器事件作為示例在對(duì)應(yīng)平臺(tái)端生成流事件罐监。 主要關(guān)注的是確保我們正在監(jiān)聽(tīng)來(lái)自平臺(tái)源(在這種情況下為傳感器管理器)的事件吴藻,并在以下情況下通過(guò)事件通道發(fā)送它們:1)Dart側(cè)至少有一個(gè)流監(jiān)聽(tīng)器和2)activity正在運(yùn)行。 在單個(gè)類中打包必要的邏輯會(huì)增加正確執(zhí)行此操作的可能性:
// 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()
}
}
如你在你的app中使用了 android.arch.lifecycle
包, 你可以通過(guò)使用LifecycleObserver來(lái)更加方便地管理SensorListener
弓柱。
要點(diǎn)
stream handler的生命周期沟堡。 在平臺(tái)端,stream handler有兩個(gè)方法onListen和onCancel矢空,只要Dart流的偵聽(tīng)器數(shù)分別從0變?yōu)?和或者從1變?yōu)?航罗,就會(huì)調(diào)用它們。 這可能會(huì)發(fā)生多次屁药。 stream handler實(shí)現(xiàn)應(yīng)該在調(diào)用前者(onListen)時(shí)開(kāi)始將事件傾注到事件接收器中粥血,并在調(diào)用后者(onCancel)時(shí)停止。 此外,stream handler應(yīng)該在程序處于非活躍狀態(tài)時(shí)暫停复亏。 上面的代碼提供了一個(gè)典型示例趾娃。在底層上,stream handler當(dāng)然只是一個(gè)二進(jìn)制消息處理程序缔御,使用事件通道的名稱在Flutter視圖中注冊(cè)抬闷。
編解碼器。 event channel 配置有方法編解碼器刹淌,允許我們區(qū)分成功和失敗結(jié)果饶氏,這和method channel區(qū)分成功和失敗是一樣的。
Stream handler的參數(shù)和錯(cuò)誤有勾。 Stream Hanlder中onListen
和onCancel
的調(diào)用是通過(guò)調(diào)用method channel的實(shí)現(xiàn)的疹启。 因此,我們從Dart到平臺(tái)的控制方法調(diào)用和反向的事件消息都在同一個(gè)邏輯通道上蔼卡。 此設(shè)置允許將參數(shù)中繼到兩種控制方法以及要報(bào)告的任何錯(cuò)誤喊崖。 在Dart端,參數(shù)(如果有的話)在receiveBroadcastStream
的調(diào)用中給出雇逞。 這意味著它們只被指定一次荤懂,無(wú)論在流的生命周期中發(fā)生的onListen和onCancel的調(diào)用次數(shù)如何。 返回的任何錯(cuò)誤都會(huì)被記錄塘砸。
End of stream. An event sink has an endOfStream method that can be invoked to signal that no additional success or error events will be sent. The null binary message is used for this purpose. On receipt on the Dart side, the stream is closed.
流的終止节仿。 eventSink有個(gè)方法叫endOfStream,可以調(diào)用該方法以表示不會(huì)發(fā)送其他成功或錯(cuò)誤事件掉蔬。 為了這個(gè)目的實(shí)際上是使用了一個(gè)空的二進(jìn)制消息廊宪。 在Dart側(cè)收到后,流將關(guān)閉女轿。
Life of a stream. The Dart stream is backed by a stream controller fed from the incoming platform channel messages. A binary message handler is registered using the event channel’s name to receive incoming messages only while the stream has listeners.
流的生命周期箭启。 在Dart中,stream由stream controller控制的蛉迹,其消息來(lái)源于平臺(tái)通道消息傅寡。僅當(dāng)stream有l(wèi)istener的時(shí)候,使用envent channel名稱的binary message handler才會(huì)被注冊(cè)北救,以用接收消息荐操。
使用指南
使用域名作為channel名稱前綴以確保唯一性
Channel名稱只是字符串,但在我們的應(yīng)用中必須保證所有的channel名稱是唯一的珍策,無(wú)論channel是出于什么目的淀零。 你可以使用任何合適的命名方案來(lái)實(shí)現(xiàn)這一點(diǎn)。 但是膛壹,插件中為了避免channel重名的推薦方法是使用域名和插件名稱前綴,例如some.body.example.com/sensors/foo
是用于some.body
在example.com
中開(kāi)發(fā)的名為foo
通道sensors
插件。 這樣做允許插件使用者在他們的應(yīng)用程序中組合任意數(shù)量的插件模聋,而不會(huì)有channel名稱沖突的風(fēng)險(xiǎn)肩民。
考慮將平臺(tái)通道視為模塊內(nèi)通信
在分布式系統(tǒng)中調(diào)用遠(yuǎn)程過(guò)程調(diào)用的代碼看起來(lái)與使用method channels的代碼類似:你調(diào)用字符串給出的方法并序列化你的參數(shù)和結(jié)果。 由于分布式系統(tǒng)組件通常是獨(dú)立開(kāi)發(fā)和部署的链方,因此強(qiáng)大的請(qǐng)求和回復(fù)檢查至關(guān)重要持痰,通常在網(wǎng)絡(luò)兩端以檢查和日志方式完成。
Platform channels on the other hand glue together three pieces of code that are developed and deployed together, in a single component.
另一方面祟蚀,在單一組件中工窍,Platform channels將開(kāi)發(fā)的三端代碼和部署粘合在一起。
Java/Kotlin ? Dart ? Objective-C/Swift
實(shí)際上前酿,將單獨(dú)的代碼模塊打包這樣的三元組通常來(lái)說(shuō)是有意義的患雏,例如Flutter插件。 這意味著罢维,對(duì)通過(guò)method channel調(diào)用的參數(shù)與結(jié)果的檢查的是必要性如同在同一模塊中使用正常方法時(shí)對(duì)參數(shù)和結(jié)果的檢查是一樣的淹仑。
在模塊內(nèi)部,我們主要關(guān)心的是防止編程錯(cuò)誤肺孵,而這些錯(cuò)誤超出了編譯器的靜態(tài)檢查范圍匀借,并且在運(yùn)行時(shí)沒(méi)有被檢測(cè)到,直到它們?cè)跁r(shí)間或空間上造成非本地的破壞平窘。 一種合理的編碼方便是使用指定類型或斷言使假設(shè)明確吓肋,從而使我們能夠快速而干凈地失敗,例如: 異常瑰艘。 當(dāng)然是鬼,細(xì)節(jié)因編程語(yǔ)言而異。 例子:
- 如果希望通過(guò)platform channel傳遞的數(shù)據(jù)有一個(gè)具體的類型磅叛,請(qǐng)立即將該類型分配給它屑咳。
- 如果希望通過(guò)平臺(tái)通道接收到的值是非空(non-null)的,那么可以設(shè)置一些參數(shù)使其立即取消引用弊琴,或者在存儲(chǔ)數(shù)據(jù)之前斷言它是非空的兆龙。 根據(jù)你的編程語(yǔ)言,你可以將其分配給非可空類型的變量敲董。
兩個(gè)簡(jiǎn)單的例子:
// Dart: 我們期望接收到一個(gè)非空的整型list紫皇。
for (final int n in await channel.invokeMethod('getFib', 100)) {
print(n * n);
}
// Android: 我們期望異步地來(lái)處理的非空的name和age參數(shù),用鍵為字符串的map來(lái)交付腋寨。
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> T argument(String key)
方法聪铺,該方法在參數(shù)中查找鍵(假設(shè)為map),并將找到的值轉(zhuǎn)換為目標(biāo)(調(diào)用者)類型萄窜。如果由于任何原因失敗铃剔,則拋出適當(dāng)?shù)漠惓H鼋啊膍ethod call handler拋出時(shí),它將被記錄下來(lái)键兜,并將錯(cuò)誤結(jié)果發(fā)送到Dart端凤类。
不要 mock platform channels
(Pun intended.) When writing unit tests for Dart code that uses platform channels, a knee jerk reaction may be to mock the channel object, as you would a network connection.
(雙關(guān)語(yǔ))。當(dāng)為使用platform channels的Dart代碼編寫(xiě)單元測(cè)試時(shí)普气,一個(gè)下意識(shí)的反應(yīng)可能是模擬channel對(duì)象谜疤,就像模擬網(wǎng)絡(luò)連接一樣。
You can certainly do that, but channel objects don’t actually need to be mocked to play nicely with unit tests. Instead, you can register mock message or method handlers to play the role of the platform during a particular test. Here is a unit test of a function hello that is supposed to invoke the bar method on channel foo:
你當(dāng)然可以這樣做现诀,但實(shí)際上channel對(duì)象不需要為了迎合單元測(cè)試被模擬夷磕。 相反,你可以注冊(cè)模擬消息或method handlers仔沿,以在特定測(cè)試期間扮演平臺(tái)的角色坐桩。 這是一個(gè)名為hello
的函數(shù)的單元測(cè)試,它應(yīng)該在名為foo
的channel上調(diào)用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');
});
To test code that sets up message or method handlers, you can synthesize incoming messages using BinaryMessages.handlePlatformMessage. At present, this method is not mirrored on platform channels, though that could easily be done as indicated in the code below. The code defines a unit test of a class Hello that is supposed to collect incoming arguments of calls to method bar on channel foo, while returning greetings:
要測(cè)試設(shè)置消息或 method handlers的代碼于未,可以使用BinaryMessages.handlePlatformMessage
合成傳入消息撕攒。目前,這個(gè)方法在platform channels上還沒(méi)有鏡像烘浦,不過(guò)可以像下面的代碼中所示的那樣輕松地實(shí)現(xiàn)抖坪。這段代碼定義了一個(gè)類名為Hello
的單元測(cè)試,它應(yīng)該收集名在為foo
的chnnael上名為bar方法的傳入?yún)?shù)闷叉,同時(shí)返回greeting:
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;
}
上面的兩個(gè)例子都在單元測(cè)試中聲明了channel對(duì)象擦俐。這工作得很好——除非你擔(dān)心重復(fù)的通道名稱和編解碼器——因?yàn)樗芯哂邢嗤Q和編解碼器的通道對(duì)象都是等價(jià)的。你可以通過(guò)將channel聲明為const
握侧,使其對(duì)生產(chǎn)代碼和測(cè)試都可見(jiàn)蚯瞧,從而避免重復(fù)。
你不需要的是提供一種將模擬通道注入生產(chǎn)代碼的方法品擎。
考慮對(duì)平臺(tái)交互進(jìn)行自動(dòng)化測(cè)試
Platform channels非常簡(jiǎn)單埋合,但是通過(guò)由單獨(dú)的Java / Kotlin和Objective-C / Swift實(shí)現(xiàn)支持的自定義Dart API從Flutter UI獲取所有內(nèi)容確實(shí)需要一些小心。 在實(shí)際操作中萄传,保持設(shè)置正常運(yùn)行將需要自動(dòng)化測(cè)試以防止回歸甚颂。 單獨(dú)使用單元測(cè)試無(wú)法實(shí)現(xiàn)這一點(diǎn),因?yàn)槟阈枰粋€(gè)運(yùn)行 platform channels 的真實(shí)應(yīng)用程序來(lái)實(shí)際與平臺(tái)通信秀菱。
Flutter comes with the flutter_driver integration test framework that allows you to test Flutter applications running on real devices and emulators. But flutter_driver is not currently integrated with other frameworks to enable testing across Flutter and platform components. I am confident this is one area where Flutter will improve in the future.
Flutter附帶了flutter_driver
集成測(cè)試框架振诬,允許你在真實(shí)設(shè)備和模擬器上測(cè)試運(yùn)行的Flutter應(yīng)用程序。但是衍菱,flutter_driver
目前還沒(méi)有與其他框架集成赶么,以支持跨Flutter 和平臺(tái)組件進(jìn)行測(cè)試。我相信這是Flutter 在未來(lái)將得到改善的一個(gè)領(lǐng)域脊串。
在某些情況下辫呻,你可以按原樣使用flutter_driver
來(lái)測(cè)試平臺(tái)通道使用情況清钥。 這要求你的Flutter用戶界面可用于觸發(fā)任何平臺(tái)交互,然后以足夠的細(xì)節(jié)進(jìn)行更新放闺,以使你的測(cè)試能夠確定交互的結(jié)果循捺。
如果你不處于這種情況,或者你將你的platform channel打包為flutter插件雄人,并且你需要一個(gè)模塊進(jìn)行測(cè)試,那么你可以編寫(xiě)一個(gè)簡(jiǎn)單的Flutter應(yīng)用程序用于測(cè)試念赶。 該應(yīng)用程序應(yīng)具有上述特征础钠,然后可以使用flutter_driver
執(zhí)行。 你會(huì)在Flutter GitHub(https://github.com/flutter/flutter/tree/master/dev/integration_tests/platform_interaction)中找到一個(gè)例子叉谜。
保持平臺(tái)端準(zhǔn)備好接收同步調(diào)用
platform channel只能是異步的旗吁。 但是有很多平臺(tái)API卻需要同步調(diào)用宿主程序的組件,詢問(wèn)信息或提供幫助或提供選擇窗口停局。 一個(gè)例子是Android上的Activity.onSaveInstanceState
很钓。 同步意味著必須在即將到來(lái)的調(diào)用返回之前完成所有操作。 現(xiàn)在董栽,你可能希望在此類處理中包含來(lái)自Dart端的信息码倦,但是一時(shí)當(dāng)主UI線程上的同步調(diào)用已經(jīng)處于活動(dòng)狀態(tài)時(shí),開(kāi)始發(fā)送異步消息就已經(jīng)來(lái)不及了锭碳。
The approach used by Flutter, most notably for semantics/accessibility information, is to proactively send updated (or updates to) information to the platform side whenever the information changes on the Dart side. Then, when the synchronous call arrives, the information from the Dart side is already present and available to platform side code.
Flutter使用的方法袁稽,尤其是語(yǔ)義/可訪問(wèn)性( semantics/accessibility )信息,是在Dart端信息發(fā)生變化時(shí)主動(dòng)向平臺(tái)端發(fā)送更新(或更新)信息擒抛。 然后推汽,當(dāng)進(jìn)行同步調(diào)用時(shí),來(lái)自Dart的信息已經(jīng)存在并且對(duì)平臺(tái)代碼可用歧沪。