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

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

全方位了解Flutter Platforms

前言

如果你對(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)之間傳遞,如下圖所示:

通信機(jī)制.png

摘一段官方文檔:

在客戶端,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)用命令行):

image.png

image.png

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

image.png

我們著重看一下以下三個(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)FlutteriOSAndroid的交互就是通過(guò)這個(gè)MethodChannel。MethodChannel就是我們的信使,負(fù)責(zé)dart和原生代碼通信。com.jarvanmo/fluwxMethodChannel的名字,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/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

通過(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í)了ResultFlutterResult),可以通過(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)注明出處。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容