黑马程序员技术交流社区

标题: 【上海校区】Flutter实战:手把手教你写Flutter Plugin [打印本页]

作者: 不二晨    时间: 2018-9-21 11:41
标题: 【上海校区】Flutter实战:手把手教你写Flutter Plugin
前言如果你对移动端有所关注,那么你一定会听说过Flutter。得益于Google,Flutter一经推出便得受到了广泛关注。很多开发者跃跃欲试,国内部分大厂,诸如美团、闲鱼等团队已经开始了Flutter实践之旅了。笔者也是蹭了一波热度,学习了一下Flutter。Flutter虽然真香,但目前社区显然还是很不健全,像微信SDK、支付宝等第三方SDK都无法在Flutter项目上直接使用。想要使用这些SDK就曲线救国了。本文并不探讨如何发布一个Flutter Plugin,只谈如何实现Plugin。下面我将以我的开源项目fluwx为例,手把手教你如何写Flutter Plugin。
在2018年GDD上,Flutter分会场演示代码就用到了Fluwx.详情可以戳这里
什么是Flutter PluginFlutter Plugin是一种特殊的包,一个插件包含一个用Dart编写的API定义,结合Android和iOS的平台特定实现,从而达到二者兼容。平常我们使用插件可以到这个网站去搜索。
如何与原生进行通信?消息通过platform channels在客户端(UI)和主机(platform)之间传递,如下图所示:
摘一段官方文档:
在客户端,MethodChannel(API)允许发送与方法调用相对应的消息。 在平台方 面,Android(API)上的MethodChannel和iOS(API)上的FlutterMethodChannel启用接收方法调用并发回结果。 这些类允许您使用非常少的“样板”代码开发平台插件。
所谓的客户端是指Flutter层,而平台层面则是对应Android或者iOS。至于究竟怎么使用MethodChannel,我先卖个关子,后面会具体提到。既然涉及到了Flutter与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请用命令行):



一路next就行了。一个Flutter Plugin就创建成功了,项目结构是这样的:

我们着重看一下以下三个文件:
下面我会继续以Fluwx为例逐一讲解每个参数的意义。
MethodChannel的定义首先,打开lib/src/fluwx_class.dart,我们会发现如下代码:
final MethodChannel _channel = const MethodChannel('com.jarvanmo/fluwx');复制代码重点来了,我们要实现Flutter与iOS和Android的交互就是通过这个MethodChannel。MethodChannel就是我们的信使,负责dart和原生代码通信。com.jarvanmo/fluwx是MethodChannel的名字,flutter通过一个具体的名字能才够在对应平台上找到对应的MethodChannel,从而实现flutter与平台的交互。同样地,我们在对应的平台上也要注册名为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复制代码通过上面几个步骤,我们已经完成了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个:
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。有同学可能会说,上面我们已经学习了Result(FlutterResult),可以通过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插件,并且至少掌握以下几点:



链接:https://juejin.im/post/5ba3b28e6fb9a05d171d3cba




作者: 不二晨    时间: 2018-10-10 13:34
奈斯




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2