Flutter實戰:手把手教你寫Flutter Plugin

前言

若是你對移動端有所關注,那麼你必定會據說過Flutter。得益於GoogleFlutter一經推出便得受到了普遍關注。不少開發者躍躍欲試,國內部分大廠,諸如美團、閒魚等團隊已經開始了Flutter實踐之旅了。筆者也是蹭了一波熱度,學習了一下FlutterFlutter雖然真香,但目前社區顯然仍是很不健全,像微信SDK、支付寶等第三方SDK都沒法在Flutter項目上直接使用。想要使用這些SDK就曲線救國了。 本文並不探討如何發佈一個Flutter Plugin,只談如何實現Plugin。下面我將以個人開源項目fluwx爲例,手把手教你如何寫Flutter Pluginhtml

在2018年GDD上,Flutter分會場演示代碼就用到了Fluwx.詳情能夠戳這裏java

什麼是Flutter Plugin

Flutter Plugin是一種特殊的包,一個插件包含一個用Dart編寫的API定義,結合Android和iOS的平臺特定實現,從而達到兩者兼容。 日常咱們使用插件能夠到這個網站去搜索。android

如何與原生進行通訊?

消息經過platform channels在客戶端(UI)和主機(platform)之間傳遞,以下圖所示: ios

通訊機制.png
摘一段官方文檔:

在客戶端,MethodChannel(API)容許發送與方法調用相對應的消息。 在平臺方 面,Android(API)上的MethodChannel和iOS(API)上的FlutterMethodChannel啓用接收方法調用併發回結果。 這些類容許您使用很是少的「樣板」代碼開發平臺插件。git

所謂的客戶端是指Flutter層,而平臺層面則是對應Android或者iOS。至於究竟怎麼使用MethodChannel,我先賣個關子,後面會具體提到。 既然涉及到了Flutter與Android和iOS的通訊問題,那麼咱們必定會有如下幾個疑問:github

  • MethodChannel傳遞的數據支持什麼類型?
  • Dart數據類型與Android,iOS類型的對應關係是怎樣的?

這兩個問題的答案一樣來自官方文檔:objective-c

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請用命令行): 併發

image.png

image.png

一路next就好了。 一個Flutter Plugin就建立成功了,項目結構是這樣的: app

image.png

咱們着重看一下如下三個文件:

  • 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');
複製代碼

重點來了,咱們要實現FlutteriOSAndroid的交互就是經過這個MethodChannelMethodChannel就是咱們的信使,負責dart和原生代碼通訊。com.jarvanmo/fluwxMethodChannel的名字,flutter經過一個具體的名字能纔夠在對應平臺上找到對應的MethodChannel,從而實現flutter與平臺的交互。一樣地,咱們在對應的平臺上也要註冊名爲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
複製代碼

經過上面幾個步驟,咱們已經完成了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。有同窗可能會說,上面咱們已經學習了ResultFlutterResult),能夠經過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調用原生的結果處理,如成功,錯誤等

最後

附上Fluwx。 歡迎加入OpenFlutter交流羣:892398530. 版本全部,轉載請註明出處

相關文章
相關標籤/搜索