*本篇文章已授權(quán)微信公眾號(hào) guolin_blog (郭霖)獨(dú)家發(fā)布
前言
如果你對(duì)移動(dòng)端有所關(guān)注,那么你一定會(huì)聽(tīng)說(shuō)過(guò)Flutter。得益于Google,Flutter一經(jīng)推出便得受到了廣泛關(guān)注。很多開發(fā)者躍躍欲試,國(guó)內(nèi)部分大廠,諸如美團(tuán)、閑魚等團(tuán)隊(duì)已經(jīng)開始了Flutter實(shí)踐之旅了。筆者也是蹭了一波熱度,學(xué)習(xí)了一下Flutter。Flutter雖然真香,但目前社區(qū)顯然還是很不健全,像微信SDK、支付寶等第三方SDK都無(wú)法在Flutter項(xiàng)目上直接使用。想要使用這些SDK就曲線救國(guó)了。
本文并不探討如何發(fā)布一個(gè)Flutter Plugin,只談如何實(shí)現(xiàn)Plugin。下面我將以我的開源項(xiàng)目fluwx為例,手把手教你如何寫Flutter Plugin。
在2018年GDD上,
Flutter分會(huì)場(chǎng)演示代碼就用到了Fluwx.詳情可以戳這里。
什么是Flutter Plugin
Flutter Plugin是一種特殊的包,一個(gè)插件包含一個(gè)用Dart編寫的API定義,結(jié)合Android和iOS的平臺(tái)特定實(shí)現(xiàn),從而達(dá)到二者兼容。
平常我們使用插件可以到這個(gè)網(wǎng)站去搜索。
如何與原生進(jìn)行通信?
消息通過(guò)platform channels在客戶端(UI)和主機(jī)(platform)之間傳遞,如下圖所示:

摘一段官方文檔:
在客戶端,
MethodChannel(API)允許發(fā)送與方法調(diào)用相對(duì)應(yīng)的消息。 在平臺(tái)方 面,Android(API)上的MethodChannel和iOS(API)上的FlutterMethodChannel啟用接收方法調(diào)用并發(fā)回結(jié)果。 這些類允許您使用非常少的“樣板”代碼開發(fā)平臺(tái)插件。
所謂的客戶端是指Flutter層,而平臺(tái)層面則是對(duì)應(yīng)Android或者iOS。至于究竟怎么使用MethodChannel,我先賣個(gè)關(guān)子,后面會(huì)具體提到。
既然涉及到了Flutter與Android和iOS的通信問(wèn)題,那么我們一定會(huì)有以下幾個(gè)疑問(wèn):
- MethodChannel傳遞的數(shù)據(jù)支持什么類型?
- Dart數(shù)據(jù)類型與Android,iOS類型的對(duì)應(yīng)關(guān)系是怎樣的?
這兩個(gè)問(wèn)題的答案同樣來(lái)自官方文檔:
| 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 |
至此,我們對(duì)Flutter插件有了一個(gè)簡(jiǎn)單了解,下面我們將親自動(dòng)手寫一個(gè)插件。
創(chuàng)建一個(gè)Flutter Plugin項(xiàng)目
以Android Studio為例(vscode請(qǐng)用命令行):


一路next就行了。
一個(gè)Flutter Plugin就創(chuàng)建成功了,項(xiàng)目結(jié)構(gòu)是這樣的:

我們著重看一下以下三個(gè)文件:
- lib/src/fluwx_class.dart
- android/src/main/kotlin/com/jarvan/fluwx/FluwxPlugin.kt
- ios/Classes/FluwxPlugin.m
下面我會(huì)繼續(xù)以Fluwx為例逐一講解每個(gè)參數(shù)的意義。
MethodChannel的定義
首先,打開lib/src/fluwx_class.dart,我們會(huì)發(fā)現(xiàn)如下代碼:
final MethodChannel _channel = const MethodChannel('com.jarvanmo/fluwx');
重點(diǎn)來(lái)了,我們要實(shí)現(xiàn)Flutter與iOS和Android的交互就是通過(guò)這個(gè)MethodChannel。MethodChannel就是我們的信使,負(fù)責(zé)dart和原生代碼通信。com.jarvanmo/fluwx是MethodChannel的名字,flutter通過(guò)一個(gè)具體的名字能才夠在對(duì)應(yīng)平臺(tái)上找到對(duì)應(yīng)的MethodChannel,從而實(shí)現(xiàn)flutter與平臺(tái)的交互。同樣地,我們?cè)趯?duì)應(yīng)的平臺(tái)上也要注冊(cè)名為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
通過(guò)上面幾個(gè)步驟,我們已經(jīng)完成了Flutter與原生的橋接工作了,我們繼續(xù)。
Flutter調(diào)用原生并傳遞數(shù)據(jù)
只建立橋接顯然是不能夠滿足我們的需求,我們要通過(guò)Flutter將數(shù)據(jù)傳遞到android和iOS上,進(jìn)而完成微信的注冊(cè)。上面我們提供到了MethodChannel支持的數(shù)據(jù)類型及其對(duì)應(yīng)關(guān)系,下面我們要在Flutter傳遞一組數(shù)據(jù)(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函數(shù)的作用是注冊(cè)微信,其參數(shù)的具體意義不作解釋。由示例代碼可以看到,我們將傳進(jìn)來(lái)的參數(shù)重新組裝成了Map并傳遞給了invokeMethod。其中invokeMethod函數(shù)第一個(gè)參數(shù)為函數(shù)名稱,即registerApp,我們將在原生平臺(tái)用到這個(gè)名字。第二個(gè)參數(shù)為要傳遞給原生的數(shù)據(jù)。我們看一下invokeMethod的源碼:
Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
//some code
}
很有趣的是,第二個(gè)參數(shù)是dynamic的,那么我們是否可以傳遞任何數(shù)據(jù)類型呢?至少語(yǔ)法上是沒(méi)有錯(cuò)誤的,但實(shí)際上這是不允許的,只有對(duì)應(yīng)平臺(tái)的codec支持的類型才能進(jìn)行傳遞,也就是上文提到的數(shù)據(jù)類型對(duì)應(yīng)表,這條規(guī)則同樣適用于返回值,也就是原生給Flutter傳值。請(qǐng)記住這條規(guī)定,不再做贅述。
如何在原生接收Flutter傳遞過(guò)來(lái)的數(shù)據(jù)?
上面我們將數(shù)據(jù)通過(guò)Flutter傳遞給了原生,我們要原生代碼里進(jìn)行接收與處理,先看Android的代碼:
override fun onMethodCall(call: MethodCall, result: Result): Unit {
if (call.method == "registerApp") {
WXAPiHandler.registerApp(call, result)
return
}
}
call.method是方法名稱,我們要通過(guò)方法名稱比對(duì)完成調(diào)用匹配。當(dāng)call.method == "registerApp"成立時(shí),說(shuō)明我們要調(diào)用registerApp,從而進(jìn)行更多的操作。此時(shí)可能會(huì)有同學(xué)問(wèn),如發(fā)現(xiàn)call.method不存在怎么辦?很簡(jiǎn)單,我們可以通過(guò)result向Flutter報(bào)告一下該方法沒(méi)實(shí)現(xiàn):
result.notImplemented()
當(dāng)調(diào)用這個(gè)方法之后,我們會(huì)在Flutter層收到一個(gè)沒(méi)實(shí)現(xiàn)該方法的異常。
iOS端也是大同小異的:
- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
if ([@"registerApp" isEqualToString:call.method]) {
[_fluwxWXApiHandler registerApp:call result:result];
return;
}
}
如果方法不存在:
result(FlutterMethodNotImplemented);
通過(guò)以上步驟我們已經(jīng)能夠接收到Flutter的調(diào)用了,但是我們的任務(wù)還沒(méi)完成,因?yàn)檫€沒(méi)取到我們想要的數(shù)據(jù)。參數(shù)call攜帶了由Flutter傳遞過(guò)來(lái)的數(shù)據(jù),在Android中其數(shù)據(jù)放在call.arguments,其類型為java.lang.Object,與Flutter傳遞過(guò)來(lái)數(shù)據(jù)類型一一對(duì)應(yīng)。如果數(shù)據(jù)類型是Map,我們可以通過(guò)以下方式取出對(duì)應(yīng)值:
val appId: String? = call.argument("appId")
iOS同理:
NSString *appId = call.arguments[@"appId"];
當(dāng)我們?nèi)〉搅?em>appId以后,我們就可以進(jìn)行微信注冊(cè)了,這里不做敘述。
到這里,我們已經(jīng)可以完成Flutter調(diào)用原生并接收數(shù)據(jù),從而完成微信注冊(cè)。但這樣做并不能讓我們滿意,原因有2個(gè):
- 如何告訴Flutter我們的處理結(jié)果?
- 用戶總是調(diào)皮的,如appId是一個(gè)空字符串,如何讓Flutterr拋出一個(gè)異常?
對(duì)于這2個(gè)問(wèn)題,我們?cè)缇桶l(fā)現(xiàn)在接收Flutter調(diào)用的時(shí)候會(huì)傳遞一個(gè)名字result的參數(shù),通過(guò)result我們可以向Flutter打小報(bào)告,小報(bào)告的有三種形式: - success,成功
- error,遇到錯(cuò)誤
- notImplemented,沒(méi)實(shí)現(xiàn)對(duì)應(yīng)方法
其中notImplemented,已經(jīng)說(shuō)過(guò)了。而success故名思義,就是處理成功,可以回調(diào)一些數(shù)據(jù),也可以不回傳,調(diào)用非常簡(jiǎn)單:
result.success(mapOf(
WechatPluginKeys.PLATFORM to WechatPluginKeys.ANDROID,
WechatPluginKeys.RESULT to registered
))
result(@{fluwxKeyPlatform: fluwxKeyIOS, fluwxKeyResult: @(isWeChatRegistered)});
error見(jiàn)名思義,報(bào)告錯(cuò)誤,當(dāng)我們遇到了一些異常需要回調(diào)給Flutter時(shí),這個(gè)方法就很有用了。調(diào)用這個(gè)方法會(huì)使Futter拋出一個(gè)異常。先看一下在Android上是怎么調(diào)用的:
result.error("invalid app id", "are you sure your app id is correct ?", appId)
第一個(gè)參數(shù)是errorCode(錯(cuò)誤代碼,雖然叫Code但卻是一個(gè)String),第二個(gè)參數(shù)是errorMessage(錯(cuò)誤信息),第三個(gè)details(詳情),這個(gè)詳情就是錯(cuò)誤的具體信息了,當(dāng)然也可以選擇不傳。
iOS對(duì)應(yīng)代碼如下:
result([FlutterError errorWithCode:@"invalid app id" message:@"are you sure your app id is correct ? " details:appId]);
到目前為止,我們已經(jīng)完成了一半工作,已經(jīng)完成了通過(guò)Flutter實(shí)現(xiàn)微信注冊(cè),但我們的工作永不止如此,我們還要完成通過(guò)原生調(diào)用Flutter,從而實(shí)現(xiàn)分享,支付等的回調(diào)。
注意:分享一個(gè)小坑,在iOS上,空指針有可能是
nil或者NSNull,坑就在這。如果Flutter傳來(lái)的String是null,那么在oc中對(duì)應(yīng)的是NSNull,但微信SDK的參數(shù)可以為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];
原生如何調(diào)用Flutter
當(dāng)我們完成分享時(shí),我們可能需要將分享結(jié)果傳回Flutter。有同學(xué)可能會(huì)說(shuō),上面我們已經(jīng)學(xué)習(xí)了Result(FlutterResult),可以通過(guò)result實(shí)現(xiàn)啊。但微信的這些回調(diào)是異步的,我們也不能夠長(zhǎng)期持有Result對(duì)象,所以這個(gè)時(shí)候我們要在原生中調(diào)用Flutter。
原理也一樣,在原生代碼中,我們也有一個(gè)MethodChannel:
val channel = MethodChannel(registrar.messenger(), "com.jarvanmo/fluwx")
FlutterMethodChannel *channel = [FlutterMethodChannel
methodChannelWithName:@"com.jarvanmo/fluwx"
binaryMessenger:[registrar messenger]];
當(dāng)我們拿到了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];
原生調(diào)用Flutter和Flutter調(diào)用原生的方式其實(shí)是一樣的,都是通過(guò)MethodChannel調(diào)用指定名稱的方法,并傳遞數(shù)據(jù)。那么,F(xiàn)lutter的接受原生調(diào)用的方式和原生接收Flutter調(diào)用的方式應(yīng)該也是樣的:
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;
當(dāng)然了不使用Stream也可以。通過(guò)Stream,我們可以更輕松地監(jiān)聽(tīng)回調(diào)數(shù)據(jù)變化:
_fluwx.response.listen((data) {
//do something
});
至此,我們已經(jīng)完成了微信的注冊(cè)以及微信回調(diào)的回傳,剩下的工作是不是可以自己完成啦?
總結(jié)
通過(guò)本文的學(xué)習(xí),我們已經(jīng)了解了如何親手編寫一個(gè)Flutter插件,并且至少掌握以下幾點(diǎn):
- 創(chuàng)建一個(gè)Flutter Plugin項(xiàng)目
- Flutter調(diào)用原生
- 原生調(diào)用Flutter
- Flutter調(diào)用原生的結(jié)果處理,如成功,錯(cuò)誤等
最后
附上Fluwx。同時(shí),OpenFlutter歡迎各位開源愛(ài)好者分享自己的作品,郵箱:jarvan.mo@gmail.com。QQ群:892398530。
版本所有,轉(zhuǎn)載請(qǐng)注明出處。