Flutter实战:手把手教你写Flutter Plugin

简介: *本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布前言如果你对移动端有所关注,那么你一定会听说过Flutter。得益于Google,Flutter一经推出便得受到了广泛关注。

*本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布

前言

如果你对移动端有所关注,那么你一定会听说过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)之间传递,如下图所示:

img_e8177896a13cf0a661c1d92b6e598920.png
通信机制.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请用命令行):

img_7e2487e98c324728737eb713bb1cf8d4.png
image.png

img_81fd81badbe89349728d0a8c54b2cfa4.png
image.png

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

img_72b3777bd8bc0e7161ba481df3453c8e.png
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。
版本所有,转载请注明出处

目录
相关文章
|
2月前
|
Dart Android开发
鸿蒙Flutter实战:05-使用第三方插件
在鸿蒙Flutter开发中,使用原生功能需借助插件。可自编原生ArkTS代码或采用第三方插件。自编代码通过PlatformView或MethodChannel实现;第三方插件需确保适配鸿蒙,否则须配置替代插件或自行开发。
96 1
鸿蒙Flutter实战:05-使用第三方插件
|
2月前
|
Java 开发工具
鸿蒙Flutter实战:02-Windows环境搭建踩坑指南
本指南介绍如何搭建鸿蒙Flutter开发环境,包括下载Flutter SDK、配置环境变量(如FLUTTER_STORAGE_BASE_URL、PUB_HOSTED_URL、DEVECO_SDK_HOME等)和检查工具版本。还提到避免项目路径过深、与SDK同盘存放等注意事项,以及解决VsCode无法识别设备的方法。
62 0
|
1月前
|
存储 调度 数据安全/隐私保护
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
鸿蒙应用打包上架流程包括创建应用、打包签名和上传应用。首先,在AppGallery Connect中创建项目、APP ID和元服务。接着,使用Deveco进行手动签名,生成.p12和.csr文件,并在AppGallery Connect中上传CSR文件获取证书。最后,配置签名并打包生成.app文件,上传至应用市场。常见问题包括检查签名配置文件是否正确。参考资料:[应用/服务签名](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-V5/ide-signing-V5)。
66 3
鸿蒙Flutter实战:13-鸿蒙应用打包上架流程
|
1月前
|
开发工具 芯片 开发者
鸿蒙Flutter实战:12-使用模拟器开发调试
本文介绍了如何在 M 系列芯片的 Mac 电脑上使用模拟器进行鸿蒙 Flutter 开发和调试。主要内容包括:创建 Flutter 项目、签名、创建模拟器、运行 Flutter 项目以及常见问题的解决方法。适用于希望在鸿蒙系统上开发 Flutter 应用的开发者。
57 2
鸿蒙Flutter实战:12-使用模拟器开发调试
|
1月前
|
IDE 开发工具
鸿蒙Flutter实战:11-使用 Flutter SDK 3.22.0
本文介绍了如何使用 Flutter SDK 3.22.0 搭建鸿蒙开发环境。首先安装 Flutter SDK 3.22.0,并通过 FVM 管理多个版本。接着配置项目,使用 `fvm use custom_3.22.0` 设置自定义 SDK 版本。添加鸿蒙平台支持并进行项目签名,最后通过 `fvm flutter run` 运行项目。详细步骤包括安装、项目配置、签名和运行,确保开发环境顺利搭建。
133 7
鸿蒙Flutter实战:11-使用 Flutter SDK 3.22.0
|
1月前
|
UED
<大厂实战经验> Flutter&鸿蒙next 中使用 initState 和 mounted 处理异步请求的详细解析
在 Flutter 开发中,处理异步请求是常见需求。本文详细介绍了如何在 `initState` 中触发异步请求,并使用 `mounted` 属性确保在适当时机更新 UI。通过示例代码,展示了如何安全地进行异步操作和处理异常,避免在组件卸载后更新 UI 的问题。希望本文能帮助你更好地理解和应用 Flutter 中的异步处理。
74 3
|
1月前
|
JavaScript API 开发工具
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
本文介绍了如何在 Flutter 中解析后端返回的 HTML 数据。首先解释了 HTML 解析的概念,然后详细介绍了使用 `http` 和 `html` 库的步骤,包括添加依赖、获取 HTML 数据、解析 HTML 内容和在 Flutter UI 中显示解析结果。通过具体的代码示例,展示了如何从 URL 获取 HTML 并提取特定信息,如链接列表。希望本文能帮助你在 Flutter 应用中更好地处理 HTML 数据。
119 1
|
2月前
|
开发者
鸿蒙Flutter实战:07-混合开发
鸿蒙Flutter混合开发支持两种模式:1) 基于har包,便于主项目开发者无需关心Flutter细节,但不支持热重载;2) 基于源码依赖,利于代码维护与热重载,需配置Flutter环境。项目结构包括AppScope、flutter_module等目录,适用于不同开发需求。
105 3
|
2月前
|
Web App开发 JavaScript 前端开发
鸿蒙Flutter实战:04-如何使用DevTools调试Webview
本文介绍如何在鸿蒙 Flutter 开发中调试 Webview,包括配置允许调试、找到 devtools 端口、开启端口转发、在 Chrome 中调试 Webview等。
47 0
鸿蒙Flutter实战:04-如何使用DevTools调试Webview
|
2月前
|
开发工具 Android开发 git
鸿蒙Flutter实战:01-搭建开发环境
本文介绍了如何搭建鸿蒙 Flutter 开发环境,包括安装 DevEco Studio 等工具,并详细说明了 Mac 和 Windows 系统下的环境变量配置。此外,还介绍了如何使用 FVM 管理多个 Flutter 版本,并提供了一些常见问题的解决方案和交流群信息。
138 0
鸿蒙Flutter实战:01-搭建开发环境