前言
我們都知道Flutter開發的app是可以同時在iOS和Android系統上執行的。顯然Flutter需要有和Native通訊的能力。比如說,你的Flutter app要顯示手機的電量,而電量只能通過平臺的系統Api獲取。這時就需要有個機制使得Flutter可以通過某種方式來呼叫這個系統Api並且獲得返回值。那麼Flutter是如何做到的呢?答案是Platform Channels。
Platform Channels
先來看張圖
上圖來自Flutter官網,表明了Platform Channels的架構示意圖。有細心的同學就要問了,你不是說Flutter和Native通訊是通過Platform Channels嗎?怎麼架構圖裡面連線他們的是MethodChannel? 其實呢,MethodChannel是Platform Channels中的一種,顧名思義,MethodChannel用起來應該和方法呼叫差不多。那麼還有別的channel?有的,還有EventChannel,BasicMessageChannel等。如果你需要把資料從Native平臺傳送給Flutter,推薦你使用EventChannel。Flutter framework也是在用這些通道和Native通訊,具體可以參考一下FlutterView.java
,在這裡能看到Platform Channels的更多用法。
這裡需要注意一點,為了保證UI的響應,通過Platform Channels傳遞的訊息都是非同步的。
在Platform Channels上傳遞的訊息都是經過編碼的,編碼的方式也有幾種,預設的是用StandardMethodCodec
。其他的還有BinaryCodec
(二進位制的編碼,其實啥也沒幹,直接把入參給返回了), JSONMessageCodec
(JSON格式的編碼),StringCodec
(String格式的編碼)。這些編解碼器允許的只能是以下這些型別:
com.yourmodule.YourObject
型別的一個例項直接扔給Platform Channels傳送是不行滴。
Platform Channels 怎麼用
前面大概介紹了Flutter和Native通訊的Platform Channels。那麼我們用具體的例子來說說Platform Channels的使用。這裡使用Flutter官方出的獲取手機電量的Demo。相關原始碼可以從Github下載。
Platform Channels是連線Flutter和Native的通道,那麼我們如果要建立這樣的通道顯然要在兩端都要寫程式碼嘍。
MethodChannel
先看Native 端怎麼寫
MethodChannel-Native 端
為簡單起見,本例的Android端程式碼都直接寫在MainActivity
中。Android平臺下獲取電量是通過呼叫BatteryManager來獲取的,所以我們先在MainActivity
中增加一個獲取電量的函式:
private int getBatteryLevel() {
int batteryLevel = -1;
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
BatteryManager batteryManager = (BatteryManager) getSystemService(BATTERY_SERVICE);
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY);
} else {
Intent intent = new ContextWrapper(getApplicationContext()).
registerReceiver(null, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
batteryLevel = (intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100) /
intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
}
return batteryLevel;
}
複製程式碼
這個函式需要能被Flutter app呼叫,此時就需要通過MethodChannel
來建立這個通道了。
首先在MainActivity
的onCreate
函式中加入以下程式碼來新建一個MethodChannel
public class MainActivity extends FlutterActivity {
//channel的名稱,由於app中可能會有多個channel,這個名稱需要在app內是唯一的。
private static final String CHANNEL = "samples.flutter.io/battery";
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
// 直接 new MethodChannel,然後設定一個Callback來處理Flutter端呼叫
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
@Override
public void onMethodCall(MethodCall call, Result result) {
// 在這個回撥裡處理從Flutter來的呼叫
}
});
}
}
複製程式碼
注意,每個
MethodChannel
需要有唯一的字串作為標識,用以互相區分,這個名稱建議使用package.module...
這樣的模式來命名。因為所有的MethodChannel
都是儲存在以通道名為Key的Map中。所以你要是設了兩個名字一樣的channel,只有後設定的那個會生效。
接下來我們來填充onMethodCall
。
@Override
public void onMethodCall(MethodCall call, Result result) {
if (call.method.equals("getBatteryLevel")) {
int batteryLevel = getBatteryLevel();
if (batteryLevel != -1) {
result.success(batteryLevel);
} else {
result.error("UNAVAILABLE", "Battery level not available.", null);
}
} else {
result.notImplemented();
}
}
複製程式碼
onMethodCall
有兩個入參,MethodCall
裡包含要呼叫的方法名稱和引數。Result
是給Flutter的返回值。方法名是兩端協商好的。通過if語句判斷MethodCall.method
來區分不同的方法,在我們的例子裡面我們只會處理名為“getBatteryLevel”的呼叫。在呼叫本地方法獲取到電量以後通過result.success(batteryLevel)
呼叫把電量值返回給Flutter。
Native端的程式碼就完成了。是不是很簡單?
MethodChannel-Flutter 端
接下來看Flutter端程式碼怎麼寫:
首先在 State
中建立Flutter端的MethodChannel
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
...
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('samples.flutter.io/battery');
// Get battery level.
}
複製程式碼
channel的名稱要和Native端的一致。 然後是通過MethodChannel呼叫的程式碼
String _batteryLevel = 'Unknown battery level.';
Future<Null> _getBatteryLevel() async {
String batteryLevel;
try {
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = 'Battery level at $result % .';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
複製程式碼
final int result = await platform.invokeMethod('getBatteryLevel');
這行程式碼就是通過通道來呼叫Native方法了。注意這裡的await
關鍵字。前面我們說過MethodChannel是非同步的,所以這裡必須要使用await
關鍵字。
在上面Native程式碼中我們把獲取到的電量通過result.success(batteryLevel);
返回給Flutter。這裡await
表示式執行完成以後電量就直接賦值給result
變數了。剩下的就是怎麼展示的問題了,就不再細說了,具體可以去看程式碼。
需要注意的是,這裡我們只介紹了從Flutter呼叫Native方法,其實通過MethodChannel
,Native也能呼叫Flutter的方法,這是一個雙向的通道。
舉個例子,我們想從Native端請求Flutter端的一個getName
方法獲取一個字串。在Flutter端你需要給MethodChannel
設定一個MethodCallHandler
_channel.setMethodCallHandler(platformCallHandler);
Future<dynamic> platformCallHandler(MethodCall call) async {
switch (call.method) {
case "getName":
return "Hello from Flutter";
break;
}
}
複製程式碼
在Native端,只需要讓對應的的channel呼叫invokeMethod
就行了
channel.invokeMethod("getName", null, new MethodChannel.Result() {
@Override
public void success(Object o) {
// 這裡就會輸出 "Hello from Flutter"
Log.i("debug", o.toString());
}
@Override
public void error(String s, String s1, Object o) {
}
@Override
public void notImplemented() {
}
});
複製程式碼
至此,MethodChannel
的用法就介紹完了。可以發現,通過MethodChannel
Native和Flutter方法互相呼叫還是蠻直接的。這裡只是做了個大概的介紹,具體細節和一些複雜用法還有待大家的探索。
MethodChannel
提供了方法呼叫的通道,那如果Native有資料流需要傳送給Flutter該怎麼辦呢?這時候就要用到EventChannel
了。
EventChannel
EventChannel
的使用我們也以官方獲取電池電量的demo為例,手機的電池狀態是不停變化的。我們要把這樣的電池狀態變化由Native及時通過EventChannel
來告訴Flutter。這種情況用之前講的MethodChannel
辦法是不行的,這意味著Flutter需要用輪詢的方式不停呼叫getBatteryLevel
來獲取當前電量,顯然是不正確的做法。而用EventChannel
的方式,則是將當前電池狀態"推送"給Flutter.
EventChannel - Native端
先看我們熟悉的Native端怎麼來建立EventChannel
, 還是在MainActivity.onCreate
中,我們加入如下程式碼:
new EventChannel(getFlutterView(), "samples.flutter.io/charging").setStreamHandler(
new StreamHandler() {
// 接收電池廣播的BroadcastReceiver。
private BroadcastReceiver chargingStateChangeReceiver;
@Override
// 這個onListen是Flutter端開始監聽這個channel時的回撥,第二個引數 EventSink是用來傳資料的載體。
public void onListen(Object arguments, EventSink events) {
chargingStateChangeReceiver = createChargingStateChangeReceiver(events);
registerReceiver(
chargingStateChangeReceiver, new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
}
@Override
public void onCancel(Object arguments) {
// 對面不再接收
unregisterReceiver(chargingStateChangeReceiver);
chargingStateChangeReceiver = null;
}
}
);
複製程式碼
和MethodChannel
類似,我們也是直接new一個EventChannel
例項,並給它設定了一個StreamHandler
型別的回撥。其中onCancel
代表對面不再接收,這裡我們應該做一些clean up的事情。而 onListen
則代表通道已經建好,Native可以傳送資料了。注意onListen
裡帶的EventSink
這個引數,後續Native傳送資料都是經過EventSink
的。看程式碼:
private BroadcastReceiver createChargingStateChangeReceiver(final EventSink events) {
return new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
int status = intent.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
if (status == BatteryManager.BATTERY_STATUS_UNKNOWN) {
events.error("UNAVAILABLE", "Charging status unavailable", null);
} else {
boolean isCharging = status == BatteryManager.BATTERY_STATUS_CHARGING ||
status == BatteryManager.BATTERY_STATUS_FULL;
// 把電池狀態發給Flutter
events.success(isCharging ? "charging" : "discharging");
}
}
};
}
複製程式碼
在onReceive
函式內,系統發來電池狀態廣播以後,在Native這裡轉化為約定好的字串,然後通過呼叫events.success();
傳送給Flutter。Native端的程式碼就是這樣,接下來看Flutter端。
EventChannel - Flutter端
首先還是在State內建立EventChannel
static const EventChannel eventChannel =
const EventChannel('samples.flutter.io/charging');
複製程式碼
然後在initState
的時候開啟這個channel:
@override
void initState() {
super.initState();
eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
複製程式碼
收到event以後的處理是在_onEvent
函式裡:
void _onEvent(Object event) {
setState(() {
_chargingStatus =
"Battery status: ${event == 'charging' ? '' : 'dis'}charging.";
});
}
void _onError(Object error) {
setState(() {
_chargingStatus = 'Battery status: unknown.';
});
}
複製程式碼
從Native端傳過來的"charging"/"discharging"字串直接就是入參event
。好了,Flutter端的程式碼也貼完了,是不是感覺EventChannel
用起來也很簡單?
收尾
至此,本文對Flutter和Native之間互相通訊的方式的講解也要告一段落了。Flutter的出發點就是跨平臺,而真正要做到跨平臺則取決於Flutter是否能通過簡單的方式與Native高效通訊。Platform Channels能否實現這個目標還有待大規模應用的檢驗。對於Flutter開發者來講,由於眾多的Native平臺API需要暴露給Flutter,還有很多用Native實現的元件/業務邏輯也可能需要暴露給Flutter。這需要寫大量的通道程式碼,也就是說我們必須掌握使用Platform Channels的技能,才能體會到Flutter真正的跨平臺能力。本文中對Platform Channels的應用只是非常簡單的demo。在大型app中還存在兩大挑戰,一個是大量的通道我們如何組織,如何維護。另一個是通道協議如何設計才能抹平Android和iOS之間的平臺差異,這就需要開發這對兩個平臺都非常熟悉,這個貌似更加困難。
當然了,如果你做出來了完美的通道,將平臺的某個功能(比如藍芽,GPS什麼的)包裝成了優美的Flutter API,並且希望世界上其他Flutter開發者也能使用。那麼你可以把你智慧的結晶通過釋出Flutter外掛(plugin)的方式開放給別人。下篇文章我會介紹一下如何來開發一個Flutter外掛,敬請期待。