Flutter實(shí)戰(zhàn):手把手教你寫Flutter Plugin

*本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布

全方位了解Flutter Platforms

前言

如果你對移動端有所關(guān)注元旬,那么你一定會聽說過Flutter。得益于Google预茄,Flutter一經(jīng)推出便得受到了廣泛關(guān)注铃将。很多開發(fā)者躍躍欲試项鬼,國內(nèi)部分大廠,諸如美團(tuán)劲阎、閑魚等團(tuán)隊(duì)已經(jīng)開始了Flutter實(shí)踐之旅了绘盟。筆者也是蹭了一波熱度,學(xué)習(xí)了一下Flutter悯仙。Flutter雖然真香龄毡,但目前社區(qū)顯然還是很不健全,像微信SDK锡垄、支付寶等第三方SDK都無法在Flutter項(xiàng)目上直接使用沦零。想要使用這些SDK就曲線救國了。
本文并不探討如何發(fā)布一個Flutter Plugin货岭,只談如何實(shí)現(xiàn)Plugin路操。下面我將以我的開源項(xiàng)目fluwx為例疾渴,手把手教你如何寫Flutter Plugin

在2018年GDD上屯仗,Flutter分會場演示代碼就用到了Fluwx.詳情可以戳這里搞坝。

什么是Flutter Plugin

Flutter Plugin是一種特殊的包,一個插件包含一個用Dart編寫的API定義祭钉,結(jié)合Android和iOS的平臺特定實(shí)現(xiàn)瞄沙,從而達(dá)到二者兼容。
平常我們使用插件可以到這個網(wǎng)站去搜索慌核。

如何與原生進(jìn)行通信?

消息通過platform channels在客戶端(UI)和主機(jī)(platform)之間傳遞申尼,如下圖所示:

通信機(jī)制.png

摘一段官方文檔:

在客戶端垮卓,MethodChannel(API)允許發(fā)送與方法調(diào)用相對應(yīng)的消息。 在平臺方 面师幕,Android(API)上的MethodChannel和iOS(API)上的FlutterMethodChannel啟用接收方法調(diào)用并發(fā)回結(jié)果粟按。 這些類允許您使用非常少的“樣板”代碼開發(fā)平臺插件。

所謂的客戶端是指Flutter層霹粥,而平臺層面則是對應(yīng)Android或者iOS灭将。至于究竟怎么使用MethodChannel,我先賣個關(guān)子后控,后面會具體提到庙曙。
既然涉及到了Flutter與Android和iOS的通信問題,那么我們一定會有以下幾個疑問:

  • MethodChannel傳遞的數(shù)據(jù)支持什么類型浩淘?
  • Dart數(shù)據(jù)類型與Android捌朴,iOS類型的對應(yīng)關(guān)系是怎樣的?

這兩個問題的答案同樣來自官方文檔:

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插件有了一個簡單了解砂蔽,下面我們將親自動手寫一個插件。

創(chuàng)建一個Flutter Plugin項(xiàng)目

Android Studio為例(vscode請用命令行):

image.png

image.png

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

image.png

我們著重看一下以下三個文件:

  • lib/src/fluwx_class.dart
  • android/src/main/kotlin/com/jarvan/fluwx/FluwxPlugin.kt
  • ios/Classes/FluwxPlugin.m

下面我會繼續(xù)以Fluwx為例逐一講解每個參數(shù)的意義。

MethodChannel的定義

首先极谊,打開lib/src/fluwx_class.dart诡右,我們會發(fā)現(xiàn)如下代碼:

final MethodChannel _channel = const MethodChannel('com.jarvanmo/fluwx');

重點(diǎn)來了,我們要實(shí)現(xiàn)FlutteriOSAndroid的交互就是通過這個MethodChannel怀酷。MethodChannel就是我們的信使稻爬,負(fù)責(zé)dart和原生代碼通信。com.jarvanmo/fluwxMethodChannel的名字蜕依,flutter通過一個具體的名字能才夠在對應(yīng)平臺上找到對應(yīng)的MethodChannel桅锄,從而實(shí)現(xiàn)flutter與平臺的交互琉雳。同樣地,我們在對應(yīng)的平臺上也要注冊名為com.jarvanmo/fluwxMethodChannel友瘤。
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

通過上面幾個步驟翠肘,我們已經(jīng)完成了Flutter與原生的橋接工作了,我們繼續(xù)辫秧。

Flutter調(diào)用原生并傳遞數(shù)據(jù)

只建立橋接顯然是不能夠滿足我們的需求束倍,我們要通過Flutter將數(shù)據(jù)傳遞到android和iOS上,進(jìn)而完成微信的注冊盟戏。上面我們提供到了MethodChannel支持的數(shù)據(jù)類型及其對應(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ù)的作用是注冊微信,其參數(shù)的具體意義不作解釋柿究。由示例代碼可以看到邮旷,我們將傳進(jìn)來的參數(shù)重新組裝成了Map并傳遞給了invokeMethod。其中invokeMethod函數(shù)第一個參數(shù)為函數(shù)名稱蝇摸,即registerApp婶肩,我們將在原生平臺用到這個名字。第二個參數(shù)為要傳遞給原生的數(shù)據(jù)貌夕。我們看一下invokeMethod的源碼:

Future<dynamic> invokeMethod(String method, [dynamic arguments]) async {
//some code
}

很有趣的是律歼,第二個參數(shù)是dynamic的,那么我們是否可以傳遞任何數(shù)據(jù)類型呢啡专?至少語法上是沒有錯誤的险毁,但實(shí)際上這是不允許的,只有對應(yīng)平臺的codec支持的類型才能進(jìn)行傳遞植旧,也就是上文提到的數(shù)據(jù)類型對應(yīng)表辱揭,這條規(guī)則同樣適用于返回值,也就是原生給Flutter傳值病附。請記住這條規(guī)定问窃,不再做贅述。

如何在原生接收Flutter傳遞過來的數(shù)據(jù)完沪?

上面我們將數(shù)據(jù)通過Flutter傳遞給了原生域庇,我們要原生代碼里進(jìn)行接收與處理,先看Android的代碼:

   override fun onMethodCall(call: MethodCall, result: Result): Unit {
        if (call.method == "registerApp") {
            WXAPiHandler.registerApp(call, result)
            return
        }
}

call.method是方法名稱覆积,我們要通過方法名稱比對完成調(diào)用匹配听皿。當(dāng)call.method == "registerApp"成立時,說明我們要調(diào)用registerApp宽档,從而進(jìn)行更多的操作尉姨。此時可能會有同學(xué)問,如發(fā)現(xiàn)call.method不存在怎么辦吗冤?很簡單又厉,我們可以通過result向Flutter報告一下該方法沒實(shí)現(xiàn):

result.notImplemented()

當(dāng)調(diào)用這個方法之后九府,我們會在Flutter層收到一個沒實(shí)現(xiàn)該方法的異常。
iOS端也是大同小異的:

- (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
    if ([@"registerApp" isEqualToString:call.method]) {
        [_fluwxWXApiHandler registerApp:call result:result];
        return;
    }
}

如果方法不存在:

result(FlutterMethodNotImplemented);

通過以上步驟我們已經(jīng)能夠接收到Flutter的調(diào)用了覆致,但是我們的任務(wù)還沒完成侄旬,因?yàn)檫€沒取到我們想要的數(shù)據(jù)。參數(shù)call攜帶了由Flutter傳遞過來的數(shù)據(jù)煌妈,在Android中其數(shù)據(jù)放在call.arguments儡羔,其類型為java.lang.Object,與Flutter傳遞過來數(shù)據(jù)類型一一對應(yīng)璧诵。如果數(shù)據(jù)類型是Map汰蜘,我們可以通過以下方式取出對應(yīng)值:

val appId: String? = call.argument("appId")

iOS同理:

 NSString *appId = call.arguments[@"appId"];

當(dāng)我們?nèi)〉搅?em>appId以后,我們就可以進(jìn)行微信注冊了腮猖,這里不做敘述鉴扫。
到這里,我們已經(jīng)可以完成Flutter調(diào)用原生并接收數(shù)據(jù)澈缺,從而完成微信注冊。但這樣做并不能讓我們滿意炕婶,原因有2個:

  • 如何告訴Flutter我們的處理結(jié)果姐赡?
  • 用戶總是調(diào)皮的,如appId是一個空字符串柠掂,如何讓Flutterr拋出一個異常项滑?
    對于這2個問題,我們早就發(fā)現(xiàn)在接收Flutter調(diào)用的時候會傳遞一個名字result的參數(shù)涯贞,通過result我們可以向Flutter打小報告枪狂,小報告的有三種形式:
  • success,成功
  • error,遇到錯誤
  • notImplemented,沒實(shí)現(xiàn)對應(yīng)方法
    其中notImplemented宋渔,已經(jīng)說過了州疾。而success故名思義,就是處理成功皇拣,可以回調(diào)一些數(shù)據(jù)严蓖,也可以不回傳,調(diào)用非常簡單:
 result.success(mapOf(
                WechatPluginKeys.PLATFORM to WechatPluginKeys.ANDROID,
                WechatPluginKeys.RESULT to registered
        ))
 result(@{fluwxKeyPlatform: fluwxKeyIOS, fluwxKeyResult: @(isWeChatRegistered)});

error見名思義氧急,報告錯誤颗胡,當(dāng)我們遇到了一些異常需要回調(diào)給Flutter時,這個方法就很有用了吩坝。調(diào)用這個方法會使Futter拋出一個異常毒姨。先看一下在Android上是怎么調(diào)用的:

result.error("invalid app id", "are you sure your app id is correct ?", appId)

第一個參數(shù)是errorCode(錯誤代碼,雖然叫Code但卻是一個String)钉寝,第二個參數(shù)是errorMessage(錯誤信息)弧呐,第三個details(詳情)闸迷,這個詳情就是錯誤的具體信息了,當(dāng)然也可以選擇不傳泉懦。
iOS對應(yīng)代碼如下:

result([FlutterError errorWithCode:@"invalid app id" message:@"are you sure your app id is correct ? " details:appId]);

到目前為止稿黍,我們已經(jīng)完成了一半工作,已經(jīng)完成了通過Flutter實(shí)現(xiàn)微信注冊崩哩,但我們的工作永不止如此巡球,我們還要完成通過原生調(diào)用Flutter,從而實(shí)現(xiàn)分享邓嘹,支付等的回調(diào)酣栈。

注意:分享一個小坑,在iOS上汹押,空指針有可能是nil或者NSNull矿筝,坑就在這。如果Flutter傳來的String是null棚贾,那么在oc中對應(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)我們完成分享時铸史,我們可能需要將分享結(jié)果傳回Flutter。有同學(xué)可能會說怯伊,上面我們已經(jīng)學(xué)習(xí)了ResultFlutterResult)琳轿,可以通過result實(shí)現(xiàn)啊。但微信的這些回調(diào)是異步的耿芹,我們也不能夠長期持有Result對象崭篡,所以這個時候我們要在原生中調(diào)用Flutter
原理也一樣吧秕,在原生代碼中琉闪,我們也有一個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í)是一樣的寇甸,都是通過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也可以绽淘。通過Stream涵防,我們可以更輕松地監(jiān)聽回調(diào)數(shù)據(jù)變化:

 _fluwx.response.listen((data) {
    //do something
    });

至此,我們已經(jīng)完成了微信的注冊以及微信回調(diào)的回傳,剩下的工作是不是可以自己完成啦壮池?

總結(jié)

通過本文的學(xué)習(xí)偏瓤,我們已經(jīng)了解了如何親手編寫一個Flutter插件,并且至少掌握以下幾點(diǎn):

  • 創(chuàng)建一個Flutter Plugin項(xiàng)目
  • Flutter調(diào)用原生
  • 原生調(diào)用Flutter
  • Flutter調(diào)用原生的結(jié)果處理椰憋,如成功厅克,錯誤等

最后

附上Fluwx。同時橙依,OpenFlutter歡迎各位開源愛好者分享自己的作品证舟,郵箱:jarvan.mo@gmail.com。QQ群:892398530窗骑。
版本所有女责,轉(zhuǎn)載請注明出處

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末创译,一起剝皮案震驚了整個濱河市抵知,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌软族,老刑警劉巖刷喜,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異立砸,居然都是意外死亡吱肌,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門仰禽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人纺蛆,你說我怎么就攤上這事吐葵。” “怎么了桥氏?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵温峭,是天一觀的道長。 經(jīng)常有香客問我字支,道長凤藏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任堕伪,我火速辦了婚禮揖庄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘欠雌。我一直安慰自己蹄梢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布富俄。 她就那樣靜靜地躺著禁炒,像睡著了一般而咆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上幕袱,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天暴备,我揣著相機(jī)與錄音,去河邊找鬼们豌。 笑死涯捻,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的玛痊。 我是一名探鬼主播汰瘫,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼擂煞!你這毒婦竟也來了混弥?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤对省,失蹤者是張志新(化名)和其女友劉穎蝗拿,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體蒿涎,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡哀托,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了劳秋。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片仓手。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖玻淑,靈堂內(nèi)的尸體忽然破棺而出嗽冒,到底是詐尸還是另有隱情,我是刑警寧澤补履,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布添坊,位于F島的核電站,受9級特大地震影響箫锤,放射性物質(zhì)發(fā)生泄漏贬蛙。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一谚攒、第九天 我趴在偏房一處隱蔽的房頂上張望阳准。 院中可真熱鬧,春花似錦五鲫、人聲如沸溺职。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽浪耘。三九已至乱灵,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間七冲,已是汗流浹背痛倚。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留澜躺,地道東北人蝉稳。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像掘鄙,于是被迫代替她去往敵國和親耘戚。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容