Flutter实战:手把手教你写Flutter Plugin
前言
如果你对移动端有所关注,那么你一定会听说过Flutter
。得益于Google
,Flutter
一经推出便得受到了广泛关注。很多开发者跃跃欲试,国内部分大厂,诸如美团、闲鱼等团队已经开始了Flutter
实践之旅了。笔者也是蹭了一波热度,学习了一下Flutter
。Flutter
虽然真香,但目前社区显然还是很不健全,像微信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)之间传递,如下图所示:
摘一段官方文档:
在客户端,
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请用命令行):
一路next
就行了。
一个Flutter Plugin
就创建成功了,项目结构是这样的:
我们着重看一下以下三个文件:
- 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');
重点来了,我们要实现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个:
- 如何告诉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。有同学可能会说,上面我们已经学习了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插件,并且至少掌握以下几点:
- 创建一个Flutter Plugin项目
- Flutter调用原生
- 原生调用Flutter
- Flutter调用原生的结果处理,如成功,错误等
最后
附上Fluwx。同时,OpenFlutter欢迎各位开源爱好者分享自己的作品,邮箱:jarvan.mo@gmail.com。QQ群:892398530。
版本所有,转载请注明出处。
Flutter实战:手把手教你写Flutter Plugin的更多相关文章
- 手把手教你写电商爬虫-第三课 实战尚妆网AJAX请求处理和内容提取
版权声明:本文为博主原创文章,未经博主允许不得转载. 系列教程: 手把手教你写电商爬虫-第一课 找个软柿子捏捏 手把手教你写电商爬虫-第二课 实战尚妆网分页商品采集爬虫 看完两篇,相信大家已经从开始的 ...
- [原创]手把手教你写网络爬虫(5):PhantomJS实战
手把手教你写网络爬虫(5) 作者:拓海 摘要:从零开始写爬虫,初学者的速成指南! 封面: 大家好!从今天开始,我要与大家一起打造一个属于我们自己的分布式爬虫平台,同时也会对涉及到的技术进行详细介绍.大 ...
- 手把手教你写电商爬虫-第四课 淘宝网商品爬虫自动JS渲染
版权声明:本文为博主原创文章,未经博主允许不得转载. 系列教程: 手把手教你写电商爬虫-第一课 找个软柿子捏捏 手把手教你写电商爬虫-第二课 实战尚妆网分页商品采集爬虫 手把手教你写电商爬虫-第三课 ...
- Android开发之手把手教你写ButterKnife框架(二)
欢迎转载,转载请标明出处: http://blog.csdn.net/johnny901114/article/details/52664112 本文出自:[余志强的博客] 上一篇博客Android开 ...
- 网络编程懒人入门(八):手把手教你写基于TCP的Socket长连接
本文原作者:“水晶虾饺”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.引言 好多小白初次接触即时通讯(比如:IM或者消息推送应用)时,总是不 ...
- 手把手教你写DI_0_DI是什么?
DI是什么? Dependency Injection 常常简称为:DI. 它是实现控制反转(Inversion of Control – IoC)的一个模式. fowler 大大大神 "几 ...
- 手把手教你写Sublime中的Snippet
手把手教你写Sublime中的Snippet Sublime Text号称最性感的编辑器, 并且越来越多人使用, 美观, 高效 关于如何使用Sublime text可以参考我的另一篇文章, 相信你会喜 ...
- 手把手教你写LKM rookit! 之 第一个lkm程序及模块隐藏(一)
唉,一开始在纠结起个什么名字,感觉名字常常的很装逼,于是起了个这<手把手教你写LKM rookit> 我觉得: 你们觉得:...... 开始之前,我们先来理解一句话:一切的操作都是系统调用 ...
- 只有20行Javascript代码!手把手教你写一个页面模板引擎
http://www.toobug.net/article/how_to_design_front_end_template_engine.html http://barretlee.com/webs ...
随机推荐
- testng入门教程5TestNG套件测试
TestNG套件测试 测试套件的测试是为了测试软件程序的行为或一系列行为的情况下,是一个集合.在TestNG,我们不能定义一套测试源代码,但它代表的套件是一个XML文件执行特征.这也允许灵活的配置要运 ...
- Redis的设计与实现——字典
参考博客 绝大多数语言中的字典底层实现基本上都是哈希表.哈希表中用 “负载因子” 来衡量哈希表的 空/满 程度.为了让负载因子在一定的合理范围之内,提高查询的性能,一般的做法是让哈希表扩容,然后reh ...
- 树莓派3B新版raspbian系统换国内源
树莓派新版系统更换了专门优化过的桌面环境PIXEL,正好手头有个闲置的TF卡决定刷上新版系统玩玩.下载刷系统过程很多教程页很简单.插卡,上电开机,释放卡上的剩余空间都很正常,因为树莓派官方源访问很慢下 ...
- Summary: Java Inheritance
In this tutorial we will discuss about the inheritance in Java. The most fundamental element of Java ...
- C++声明和定义
目录 1 参考 2 概念 2.1 声明 2.2 定义 3 对比 3.1 声明但不是定义的情况 3.2 声明且是定义的情况 3.3 特殊情况 1. 参考 1. <C++程序设计语言>4.9 ...
- Object-C-内存管理 对象生命周期
autoreleasepool 池子被销毁的时候被标记 autorelease 的对象调用一次release Person *p2=[[[Person alloc]init]autorelease]; ...
- Object-C-Foundation-NSNuber
NSNumber 是一个数值类型封装起来的数值. 装箱:基础类型->对象类型 NSNumber *number=[NSNumber numberWithInt:12]; 拆箱:对象类型-> ...
- Object-C-Foundation-数组排序
系统类型排序; NSArray *goodsNames =@[@"computer",@"iphone",@"ipad"]; NSArray ...
- 【译】在Asp.Net中操作PDF - iTextSharp - 利用列进行排版(转)
[译]在Asp.Net中操作PDF - iTextSharp - 利用列进行排版 在使用iTextSharp通过ASP.Net生成PDF的系列文章中,前面的文章已经讲述了iTextSharp所涵盖 ...
- python之路----面向对象进阶二
item系列 __getitem__\__setitem__\__delitem__ class Foo: def __init__(self,name,age,sex): self.name = n ...