前言
如果你對移動端有所關注,那麼你一定會聽說過Flutter
。得益於Google
,Flutter
一經推出便得受到了廣泛關注。很多開發者躍躍欲試,國內部分大廠,諸如美團、閒魚等團隊已經開始了Flutter
實踐之旅了。筆者也是蹭了一波熱度,學習了一下Flutter
。Flutter
雖然真香,但目前社群顯然還是很不健全,像微信SDK、支付寶等第三方SDK都無法在Flutter
專案上直接使用。想要使用這些SDK就曲線救國了。
本文並不探討如何釋出一個Flutter Plugin,只談如何實現Plugin。下面我將以我的開源專案fluwx為例,手把手教你如何寫Flutter Plugin
。
在2018年GDD上,
Flutter
分會場演示程式碼就用到了Fluwx
.詳情可以戳這裡。
什麼是Flutter Plugin
Flutter Plugin是一種特殊的包,一個外掛包含一個用Dart
編寫的API定義,結合Android和iOS的平臺特定實現,從而達到二者相容。
平常我們使用外掛可以到這個網站去搜尋。
如何與原生進行通訊?
訊息通過platform channels在客戶端(UI)和主機(platform)之間傳遞,如下圖所示:
摘一段官方文件:在客戶端,
MethodChannel
(API)允許傳送與方法呼叫相對應的訊息。 在平臺方 面,Android(API)上的MethodChannel
和iOS(API)上的FlutterMethodChannel
啟用接收方法呼叫併發回結果。 這些類允許您使用非常少的“樣板”程式碼開發平臺外掛。
所謂的客戶端是指Flutter層,而平臺層面則是對應Android或者iOS。至於究竟怎麼使用MethodChannel
,我先賣個關子,後面會具體提到。
既然涉及到了Flutter與Android和iOS的通訊問題,那麼我們一定會有以下幾個疑問:
- MethodChannel傳遞的資料支援什麼型別?
- Dart資料型別與Android,iOS型別的對應關係是怎樣的?
這兩個問題的答案同樣來自官方文件:
Dart | Android | iOS |
---|---|---|
null | null | nil (NSNull when nested) |
bool | java.lang.Boolean | NSNumber numberWithBool: |
int | java.lang.Integer | NSNumber numberWithInt: |
int if 32 bits not enough | java.lang.Long | NSNumber numberWithLong: |
double | java.lang.Double | NSNumber numberWithDouble: |
String | java.lang.String | NSString |
Uint8List | byte[] | FlutterStandardTypedData typedDataWithBytes: |
Int32List | int[] | FlutterStandardTypedData typedDataWithInt32: |
Int64List | long[] | FlutterStandardTypedData typedDataWithInt64: |
Float64List | double[] | FlutterStandardTypedData typedDataWithFloat64: |
List | java.util.ArrayList | NSArray |
Map | java.util.HashMap | NSDictionary |
至此,我們對Flutter外掛有了一個簡單瞭解,下面我們將親自動手寫一個外掛。
建立一個Flutter Plugin專案
以Android Studio
為例(vscode請用命令列):
一路next
就行了。
一個Flutter Plugin
就建立成功了,專案結構是這樣的:
我們著重看一下以下三個檔案:
- lib/src/fluwx_class.dart
- android/src/main/kotlin/com/jarvan/fluwx/FluwxPlugin.kt
- ios/Classes/FluwxPlugin.m
下面我會繼續以Fluwx
為例逐一講解每個引數的意義。
MethodChannel的定義
首先,開啟lib/src/fluwx_class.dart,我們會發現如下程式碼:
final MethodChannel _channel = const MethodChannel('com.jarvanmo/fluwx');
複製程式碼
重點來了,我們要實現Flutter
與iOS
和Android
的互動就是通過這個MethodChannel
。MethodChannel
就是我們的信使,負責dart
和原生程式碼通訊。com.jarvanmo/fluwx是MethodChannel
的名字,flutter通過一個具體的名字能才夠在對應平臺上找到對應的MethodChannel
,從而實現flutter與平臺的互動。同樣地,我們在對應的平臺上也要註冊名為com.jarvanmo/fluwx的MethodChannel
。
在Android
上是這樣的:
class FluwxPlugin() : MethodCallHandler {
companion object {
@JvmStatic
fun registerWith(registrar: Registrar): Unit {
val channel = MethodChannel(registrar.messenger(), "com.jarvanmo/fluwx")
channel.setMethodCallHandler(FluwxPlugin())
}
}
}
複製程式碼
再看iOS
端:
@implementation FluwxPlugin
+ (void)registerWithRegistrar:(NSObject <FlutterPluginRegistrar> *)registrar {
FlutterMethodChannel *channel = [FlutterMethodChannel
methodChannelWithName:@"com.jarvanmo/fluwx"
binaryMessenger:[registrar messenger]];
[registrar addMethodCallDelegate:instance channel:channel];
}
@end
複製程式碼
通過上面幾個步驟,我們已經完成了Flutter
與原生的橋接工作了,我們繼續。
Flutter呼叫原生並傳遞資料
只建立橋接顯然是不能夠滿足我們的需求,我們要通過Flutter將資料傳遞到android和iOS上,進而完成微信的註冊。上面我們提供到了MethodChannel
支援的資料型別及其對應關係,下面我們要在Flutter傳遞一組資料(Map):
static Future register(
{String appId,
bool doOnIOS: true,
doOnAndroid: true,
enableMTA: false}) async {
return await _channel.invokeMethod("registerApp", {
"appId": appId,
"iOS": doOnIOS,
"android": doOnAndroid,
"enableMTA": enableMTA
});
}
複製程式碼
register
函式的作用是註冊微信,其引數的具體意義不作解釋。由示例程式碼可以看到,我們將傳進來的引數重新組裝成了Map並傳遞給了invokeMethod
。其中invokeMethod
函式第一個引數為函式名稱,即registerApp,我們將在原生平臺用到這個名字。第二個引數為要傳遞給原生的資料。我們看一下invokeMethod
的原始碼:
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
//some code
}
複製程式碼
很有趣的是,第二個引數是dynamic
的,那麼我們是否可以傳遞任何資料型別呢?至少語法上是沒有錯誤的,但實際上這是不允許的,只有對應平臺的codec
支援的型別才能進行傳遞,也就是上文提到的資料型別對應表,這條規則同樣適用於返回值,也就是原生給Flutter傳值。請記住這條規定,不再做贅述。
如何在原生接收Flutter傳遞過來的資料?
上面我們將資料通過Flutter傳遞給了原生,我們要原生程式碼裡進行接收與處理,先看Android
的程式碼:
override fun onMethodCall(call: MethodCall, result: Result): Unit {
if (call.method == "registerApp") {
WXAPiHandler.registerApp(call, result)
return
}
}
複製程式碼
call.method
是方法名稱,我們要通過方法名稱比對完成呼叫匹配。當call.method == "registerApp"
成立時,說明我們要呼叫registerApp
,從而進行更多的操作。此時可能會有同學問,如發現call.method
不存在怎麼辦?很簡單,我們可以通過result
向Flutter報告一下該方法沒實現:
result.notImplemented()
複製程式碼
當呼叫這個方法之後,我們會在Flutter層收到一個沒實現該方法的異常。 iOS端也是大同小異的:
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([@"registerApp" isEqualToString:call.method]) {
[_fluwxWXApiHandler registerApp:call result:result];
return;
}
}
複製程式碼
如果方法不存在:
result(FlutterMethodNotImplemented);
複製程式碼
通過以上步驟我們已經能夠接收到Flutter的呼叫了,但是我們的任務還沒完成,因為還沒取到我們想要的資料。引數call
攜帶了由Flutter傳遞過來的資料,在Android中其資料放在call.arguments
,其型別為java.lang.Object,與Flutter傳遞過來資料型別一一對應。如果資料型別是Map
,我們可以通過以下方式取出對應值:
val appId: String? = call.argument("appId")
複製程式碼
iOS同理:
NSString *appId = call.arguments[@"appId"];
複製程式碼
當我們取到了appId以後,我們就可以進行微信註冊了,這裡不做敘述。 到這裡,我們已經可以完成Flutter呼叫原生並接收資料,從而完成微信註冊。但這樣做並不能讓我們滿意,原因有2個:
- 如何告訴Flutter我們的處理結果?
- 使用者總是調皮的,如appId是一個空字串,如何讓Flutterr丟擲一個異常?
對於這2個問題,我們早就發現在接收Flutter呼叫的時候會傳遞一個名字
result
的引數,通過result
我們可以向Flutter打小報告,小報告的有三種形式: - success,成功
- error,遇到錯誤
- notImplemented,沒實現對應方法
其中
notImplemented
,已經說過了。而success
故名思義,就是處理成功,可以回撥一些資料,也可以不回傳,呼叫非常簡單:
result.success(mapOf(
WechatPluginKeys.PLATFORM to WechatPluginKeys.ANDROID,
WechatPluginKeys.RESULT to registered
))
複製程式碼
result(@{fluwxKeyPlatform: fluwxKeyIOS, fluwxKeyResult: @(isWeChatRegistered)});
複製程式碼
error
見名思義,報告錯誤,當我們遇到了一些異常需要回撥給Flutter時,這個方法就很有用了。呼叫這個方法會使Futter丟擲一個異常。先看一下在Android上是怎麼呼叫的:
result.error("invalid app id", "are you sure your app id is correct ?", appId)
複製程式碼
第一個引數是errorCode(錯誤程式碼,雖然叫Code但卻是一個String),第二個引數是errorMessage(錯誤資訊),第三個details(詳情),這個詳情就是錯誤的具體資訊了,當然也可以選擇不傳。
iOS
對應程式碼如下:
result([FlutterError errorWithCode:@"invalid app id" message:@"are you sure your app id is correct ? " details:appId]);
複製程式碼
到目前為止,我們已經完成了一半工作,已經完成了通過Flutter實現微信註冊,但我們的工作永不止如此,我們還要完成通過原生呼叫Flutter,從而實現分享,支付等的回撥。
注意:分享一個小坑,在iOS上,空指標有可能是
nil
或者NSNull
,坑就在這。如果Flutter傳來的String是null
,那麼在oc中對應的是NSNull
,但微信SDK的引數可以為nil
,卻不能為NSNull。
WXMediaMessage *message = [WXMediaMessage messageWithTitle:(title == (id) [NSNull null]) ? nil : title
Description:(description == (id) [NSNull null]) ? nil : description
Object:ext
MessageExt:(messageExt == (id) [NSNull null]) ? nil : messageExt
MessageAction:(messageAction == (id) [NSNull null]) ? nil : messageAction
ThumbImage:thumbImage
MediaTag:(tagName == (id) [NSNull null]) ? nil : tagName];
複製程式碼
原生如何呼叫Flutter
當我們完成分享時,我們可能需要將分享結果傳回Flutter。有同學可能會說,上面我們已經學習了Result
(FlutterResult
),可以通過result實現啊。但微信的這些回撥是非同步的,我們也不能夠長期持有Result
物件,所以這個時候我們要在原生中呼叫Flutter
。
原理也一樣,在原生程式碼中,我們也有一個MethodChannel
:
val channel = MethodChannel(registrar.messenger(), "com.jarvanmo/fluwx")
複製程式碼
FlutterMethodChannel *channel = [FlutterMethodChannel
methodChannelWithName:@"com.jarvanmo/fluwx"
binaryMessenger:[registrar messenger]];
複製程式碼
當我們拿到了MethodChannel
,我們就可以搞事情了:
val result = mapOf(
errStr to response.errStr,
WechatPluginKeys.TRANSACTION to response.transaction,
type to response.type,
errCode to response.errCode,
openId to response.openId,
WechatPluginKeys.PLATFORM to WechatPluginKeys.ANDROID
)
channel?.invokeMethod("onShareResponse", result)
複製程式碼
NSDictionary *result = @{
description: messageResp.description == nil ?@"":messageResp.description,
errStr: messageResp.errStr == nil ? @"":messageResp.errStr,
errCode: @(messageResp.errCode),
type: messageResp.type == nil ? @2 :@(messageResp.type),
country: messageResp.country== nil ? @"":messageResp.country,
lang: messageResp.lang == nil ? @"":messageResp.lang,
fluwxKeyPlatform: fluwxKeyIOS
};
[methodChannel invokeMethod:@"onShareResponse" arguments:result];
複製程式碼
原生呼叫Flutter和Flutter呼叫原生的方式其實是一樣的,都是通過MethodChannel
呼叫指定名稱的方法,並傳遞資料。那麼,Flutter的接受原生呼叫的方式和原生接收Flutter呼叫的方式應該也是樣的:
final MethodChannel _channel = const MethodChannel('com.jarvanmo/fluwx')
..setMethodCallHandler(_handler);
Future<dynamic> _handler(MethodCall methodCall) {
if ("onShareResponse" == methodCall.method) {
_responseController
.add(WeChatResponse(methodCall.arguments, WeChatResponseType.SHARE));
}
return Future.value(true);
}
複製程式碼
稍微不一樣的地方就是,在Flutter中,我們使用到了Stream:
StreamController<WeChatResponse> _responseController =
new StreamController.broadcast();
Stream<WeChatResponse> get response => _responseController.stream;
複製程式碼
當然了不使用Stream
也可以。通過Stream
,我們可以更輕鬆地監聽回撥資料變化:
_fluwx.response.listen((data) {
//do something
});
複製程式碼
至此,我們已經完成了微信的註冊以及微信回撥的回傳,剩下的工作是不是可以自己完成啦?
總結
通過本文的學習,我們已經瞭解瞭如何親手編寫一個Flutter外掛,並且至少掌握以下幾點:
- 建立一個Flutter Plugin專案
- Flutter呼叫原生
- 原生呼叫Flutter
- Flutter呼叫原生的結果處理,如成功,錯誤等