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


前言

如果你對移動端有所關注,那么你一定會聽說過Flutter。得益於GoogleFlutter一經推出便得受到了廣泛關注。很多開發者躍躍欲試,國內部分大廠,諸如美團、閑魚等團隊已經開始了Flutter實踐之旅了。筆者也是蹭了一波熱度,學習了一下FlutterFlutter雖然真香,但目前社區顯然還是很不健全,像微信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)之間傳遞,如下圖所示:

通信機制.png

摘一段官方文檔:

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

image.png
image.png

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

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歡迎各位開源愛好者分享自己的作品,郵箱:jarvan.mo@gmail.com。QQ群:892398530。
版本所有,轉載請注明出處


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM