创建Flutter插件工程
在Android Studio
里点击Flie
- New
- New Flutter Project
,在左侧里选中Flutter
,然后点击Next
。
- 在
Project Name
里输入项目名,只能是小写英文 - 在
Project type
里选择Plugin
- 在
Organization
里写包名,.Project Name
会拼在包名的最后面成为包名的一部分
也可以使用命令行
flutter create --org com.example --template=plugin plugin_name
来创建插件,其中com.example
就是Organization
,plugin_name
就是Project Name
点击Finish
后就成功创建一个插件工程了。
创建成功后可能默认打开的是Android
工程,点击切换为Project
。
切换后可以看到很多文件夹,我们需要关注的主要有以下4个:
-
android
目录是用来开发Android端的插件功能 -
ios
目录是用来开发iOS端的插件功能 -
lib
是实现Flutter插件接口的代码 -
example
目录是测试项目,用来测试开发出来的插件的
打开iOS工程
ios
目录只是一些零散的文件,是没有工程的,所以我们怎么打开它来编写呢???
我们注意到,在example
里也有一个iOS工程,没错,这个才是真正的工程!!!
但是我们打开发现会报错,也没有插件的文件,那怎么办呢?
在打开工程之前,我们需要在Android Studio
里的命令行执行以下命令:
cd example
flutter run
等执行完成之后,我们就可以打开Runner.xcworkspace
文件了,这个时候我们发现,多了一个Pods
工程。这个工程其实就是插件工程。
我们在Pods
工程的Development Pods
目录下,找到Project Name
的文件夹,一直展开,最后就看到插件的文件了,我们就是在这个文件下编写代码。
编写插件代码
本文采用的语言是Swift
。
找到Swift项目名Plugin.swift
这个文件,该文件就是插件的实现文件。
在register
方法里,我们注册了一个通道(已经默认注册了),通道名默认就是项目名,该名字在通信里必须是唯一的,可以修改,一旦修改,需要把dart
和android
里的名字也一并修改。
在handle
方法里,实现Flutter
调用原生的API,其中call.method
就是方法名,call.arguments
就是Flutter
传递过来的参数。使用result(返回值)
可以把结果返回给Flutter
。
当找不到方法名时,可以返回FlutterMethodNotImplemented
给Flutter
表示该方法还没实现,以此来做版本兼容。
具体实现如下:
public class SwiftNakiriPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(name: "nakiri", binaryMessenger: registrar.messenger())
let instance = SwiftNakiriPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
if call.method == "stateString" { // 获取网络状态的实现
result(ZTNetworkStateManager.shared.stateString)
} else if call.method == "bonusPoints" { // 使用参数的实现
let array = call.arguments as! Array<Int>
result(array[0] + array[1])
} else if call.method == "getPlatformVersion" { // 默认的实现
result("iOS " + UIDevice.current.systemVersion)
} else {
// 找不到方法
result(FlutterMethodNotImplemented)
}
}
}
Objective-C
的插件文件名是项目名Plugin.m
,注册方法是registerWithRegistrar
,实现插件内容的方法是handleMethodCall
使用第三方库
写插件不可避免的会用到第三方库,在使用第三方库的时候,会遇到3种情况:
- 仅原生端使用第三方库
- 仅
Flutter
端使用第三方库 - 都使用同一个第三方库
不同的情况有不同的处理方式。
仅原生端使用第三方库
当仅原生端需要依赖某些第三方库时,可以在项目名.podspec
文件里加上s.dependency '第三方库名'
,如:
s.dependency 'Alamofire'
然后打开命令行,跳转到Runner.xcworkspace
所在的目录,然后pod install
即可。
仅Flutter端使用第三方库
当仅Flutter
端需要依赖某些第三方库时,可以在pubspec.yaml
文件里的dependencies
部分,如:
dependencies:
flutter:
sdk: flutter
url_launcher: ^6.0.16
之后在Android Studio
里执行Pub get
就行了。
都使用同一个第三方库
假设Flutter
里需要用到url_launcher
,然后原生里也需要用到,那我们就得在Flutter
的pubspec.yaml
文件里的dependencies
部分添加依赖包,同时也要在iOS
端的项目名.podspec
文件里加上s.dependency 'url_launcher'
。
Flutter端实现
刚才我们已经在插件里增加了一个名字叫stateString
的方法,但是Flutter
端还没实现,我们现在去把它实现。
找到lib
文件夹下的项目名.dart
文件,里面就有一个类,类名就是项目名,我们增加一个方法用来调用iOS
端的stateString
方法,方法名不需要和iOS
端的保持一致,主要是通道里调用iOS
端的方法名就行了,代码如下:
class Nakiri {
static const MethodChannel _channel = MethodChannel('nakiri'); /// 通道名,需和iOS、android端保持一致
/// 默认实现
static Future<String?> get platformVersion async {
final String? version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
/// 实现iOS端新增的方法
static Future<String> stateString() async {
final String state = await _channel.invokeMethod('stateString');
return state;
}
/// 实现iOS端新增的方法
static Future<int> add() async {
final int result = await _channel.invokeMethod('bonusPoints', [5, 8]); /// 接收一个数组或者字典作为参数传递给原生端
return result;
}
}
Flutter端测试的实现
在example
目录里的lib
目录,里面有一个main.dart
文件,该文件就是测试使用的文件,我们在它本来的实现上修改一下,代码如下:
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
String _stateString = 'Unknown';
int _add = -2;
@override
void initState() {
super.initState();
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
String stateString;
int add;
try {
platformVersion =
await Nakiri.platformVersion ?? 'Unknown platform version';
stateString =
await Nakiri.stateString();
add =
await Nakiri.add();
} on PlatformException {
platformVersion = 'Failed to get platform version.';
stateString = 'Failed to get stateString';
add = -1;
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
_stateString = stateString;
_add = add;
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Center(
child: Text('Running on: $_platformVersion\n'),
),
Center(
child: Text('Network is: $_stateString\n'),
),
Center(
child: Text('Bonus points is: $_add\n'),
),
],
),
),
);
}
}
需要注意的是,
Flutter
和原生通信都是异步的,所以都需要使用await
和async
通信的数据类型
原生与Flutter
互相通信时使用的数据类型是有限制的,以下是可用的数据类型:
Dart | kotlin | Java | Objective-C | Swift |
---|---|---|---|---|
null | null | null | NSNull | NSNull |
bool | Boolean | java.lang.Boolean | NSNumber numberWithBool: | NSNumber(value: Bool)或者Bool |
int 32位平台 | Int | java.lang.Integer | NSNumber numberWithInt: | NSNumber(value: Int32)或者Int32 |
int | Long | java.lang.Long | NSNumber numberWithLong: | NSNumber(value: Int)或者Int |
double | Double | java.lang.Double | NSNumber numberWithDouble: | NSNumber(value: Double)或者Double |
String | String | java.lang.String | NSString | String或者NSString |
Uint8List | ByteArray | byte[] | FlutterStandardTypedData typedDataWithBytes: | FlutterStandardTypedData(bytes: Data) |
Int32List | IntArray | int[] | FlutterStandardTypedData typedDataWithInt32: | FlutterStandardTypedData(int32: Data) |
Int64List | LongArray | long[] | FlutterStandardTypedData typedDataWithInt64: | FlutterStandardTypedData(int64: Data) |
Float32List | FloatArray | float[] | FlutterStandardTypedData typedDataWithFloat32: | FlutterStandardTypedData(float32: Data) |
Float64List | DoubleArray | double[] | FlutterStandardTypedData typedDataWithFloat64: | FlutterStandardTypedData(float64: Data) |
List | List | java.util.ArrayList | NSArray | Array或者NSArray |
Map | HashMap | java.util.HashMap | NSDictionary | Dictionary或者NSDictionary |
- 从表里可知,
Swift
的基础类型可以用Objective-C
的对象类型,集合类型可以兼容Objective-C
的集合类型(不过这些都是Swift
本身的特性)- 在使用
Swift
时,最好还是使用它本身的类型,如果使用Objective-C
的类型,就无法判断详细类型,比如Int
和Double
,在使用Objective-C
类型的时候,都是NSNumber
原生与Flutter通信
上面都是讲Flutter
怎么调原生的,那原生能不能主动去调Flutter
呢?
我们先看看Flutter
提供的3个通信类:
-
FlutterMethodChannel
用于方法调用 -
FlutterBasicMessageChannel
用于传递简单数据 -
FlutterEventChannel
用于监听数据流
每种Channel
均有三个重要成员变量:
-
name
:String类型,代表Channel的名字,也是其唯一标识符 -
messager
:BinaryMessenger类型,代表消息信使,是消息的发送与接收的工具 -
codec
: MessageCodec类型或MethodCodec类型,代表消息的编解码器
除了需要自定义
name
之外,其余变量用默认值即可
前两种Channel
都提供了原生和Flutter
互相通信的能力,而FlutterEventChannel
不支持Flutter
端发送数据,由此可见,它们的应用场合不太一样,接下来我会讲解它们每个的使用方法。
所有的
Channel
都需要名字,在一个项目中可能会有很多的Channel
,每个Channel
都应该使用唯一的命名标识,否则可能会被覆盖。当有消息从Flutter
端发送到原生端时,会根据其传递过来的名字找到该Channel
对应的Handler
(消息处理器)
推荐的命名方式是组织名称加插件的名称,例如:com.nakiri.ayame/native_image_view,如果一个插件中包含了多个Channel
可再根据功能模块进一步进行区分
FlutterMethodChannel
通过前面我们已经知道Flutter
是通过FlutterMethodChannel
去调原生方法的,接下来我们在原生端也使用FlutterMethodChannel
去调Flutter
的方法。
首先我们改造下原生端的插件类,在里面定义一个属性,同时把register
方法注册的FlutterMethodChannel
赋值给该属性。
private static var methodChannel: FlutterMethodChannel!
methodChannel = FlutterMethodChannel(name: "nakiri", binaryMessenger: registrar.messenger())
然后我们就可以通过FlutterMethodChannel
的invokeMethod
方法调用Flutter
的方法了。
// 调用Flutter方法
SwiftNakiriPlugin.methodChannel.invokeMethod("updateNumber", arguments: (number + 1)) { value in
result(value) // 获取Flutter方法的返回值,并返回给Flutter
}
然后在Flutter
端插件代码中增加监听方法:
static int number = 1;
/// 当需要原生调用Flutter方法时,请先调用下初始化方法来增加监听
static void init() {
/// 设置原生调用Flutter时的回调
_channel.setMethodCallHandler((call) async {
switch(call.method) {
case "updateNumber":
return _updateNumber(call.arguments); /// 把结果返回给原生端
default:
break;
}
});
}
/// 实现原生调用Flutter方法
static int _updateNumber(int value) {
return number + value;
}
FlutterBasicMessageChannel
相对于FlutterMethodChannel
需要绑定代理,FlutterBasicMessageChannel
在处理消息上更为方便灵活,并且能发送大内存数据块的数据。
Flutter端使用FlutterBasicMessageChannel发送数据给原生端
流程如下:
- 原生端创建
FlutterBasicMessageChannel
- 原生端使用
setMessageHandler
方法,设置该Channel
的MessageHandler
回调 -
Flutter
端创建该name
的BasicMessageChannel
-
Flutter
端使用该BasicMessageChannel
通过send
方法向原生端发送消息 - 原生端的
MessageHandler
收到Flutter
端发送的消息,在闭包中获取到值,并且可以通过reply
闭包给Flutter
端回复 -
Flutter
端处理该回复
原生端的插件类增加的代码:
private static var basicMessageChannel: FlutterBasicMessageChannel!
// 原生和Flutter互相发送数据
basicMessageChannel = FlutterBasicMessageChannel(name: "flutter_plugin_basic_nakiri", binaryMessenger: registrar.messenger())
// 设置消息接收器,用来接收数据;当Flutter端发送消息过来的时候,会自动回调;设置为nil时取消监听
basicMessageChannel.setMessageHandler { value, reply in
reply(value as! Int + 1) // 使用reply给Flutter端回复消息
}
Flutter
端插件只需要增加一个属性即可:
static const BasicMessageChannel basicMessageChannel = BasicMessageChannel("flutter_plugin_basic_nakiri", StandardMessageCodec()); /// 定义一个渠道事件监听;名字需要唯一且各端保持一致
然后在Flutter
测试工程里,对原生发送数据:
Nakiri.basicMessageChannel.send(_basicNumber).then((value) {
setState(() {
_basicNumber = value;
});
});
原生端使用FlutterBasicMessageChannel发送数据给Flutter端
流程如下:
-
Flutter
端创建BasicMessageChannel
-
Flutter
端使用setMessageHandler
方法设置该Channel
的Handler
回调 - 原生端创建该
name
的BasicMessageChannel
- 原生端使用该
BasicMessageChannel
通过sendMessage
方法向Flutter
端发送消息 -
Flutter
端的Handler
收到发送的消息,并处理消息,然后通过return
进行回复 - 原生端处理该回复
原生端的插件类增加的代码:
// 给Flutter发送数据,并等待Flutter端回复
SwiftNakiriPlugin.basicMessageChannel.sendMessage(number + 1) { value in
result(value)
}
Flutter
端插件增加的代码:
/// 设置原生发送消息给Flutter时的回调
basicMessageChannel.setMessageHandler((message) async {
return message; /// 收到消息后,可以通过return把值回复给原生
});
FlutterEventChannel
由于FlutterEventChannel
只能原生给Flutter
发送消息,并且无返回值,所以它只能用来传输一些实时数据。
使用FlutterEventChannel
步骤如下:
-
Flutter
端定义一个EventChannel
-
Flutter
端使用receiveBroadcastStream
里的listen
监听该Channel
- 原生端定义一个
EventChannel
- 原生端通过
setStreamHandler
方法设置代理 - 原生端实现代理的
onListen
和onCancel
方法,并在onListen
里获取eventSink
闭包,在onCancel
方法里释放eventSink
闭包 - 原生端使用
eventSink
闭包方法消息 -
Flutter
端接收到消息并处理
原生端增加一个类,用来封装相关逻辑:
class ZTEventChannel: NSObject {
private var eventChannel: FlutterEventChannel?
private var eventSink: FlutterEventSink?
private var timer: Timer?
private var number = 5
override init() {
super.init()
}
required convenience init(binaryMessenger messenger: FlutterBinaryMessenger) {
self.init()
eventChannel = FlutterEventChannel(name: "flutter_plugin_event_nakiri", binaryMessenger: messenger) // 通道名必须唯一且和各端保持一致
eventChannel?.setStreamHandler(self)
}
private func removeTimer() {
timer?.invalidate()
timer = nil
number = 5
}
private func createTimer() {
if #available(iOS 10.0, *) {
if timer == nil {
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: { [weak self] (timer) in
self?.timerCall()
})
}
}
}
@objc private func timerCall() {
if let event = eventSink {
event(number)
number += 5
}
}
}
extension ZTEventChannel: FlutterStreamHandler {
func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
// 在这里获取到eventSink
self.eventSink = events
createTimer()
return nil
}
func onCancel(withArguments arguments: Any?) -> FlutterError? {
// 在这里移除eventSink
self.eventSink = nil
removeTimer()
return nil
}
}
然后在原生端插件类里使用该类:
// 原生实时发送数据流给Flutter
eventChannel = ZTEventChannel(binaryMessenger: registrar.messenger())
Flutter
端插件增加一个属性:
static const EventChannel eventChannel = EventChannel("flutter_plugin_event_nakiri"); /// 定义一个渠道事件监听;名字需要唯一且各端保持一致
然后在别的地方可以使用该属性来接收原生端发来的数据了:
_initStream() {
/// 监听原生发来的消息
_stream ??= Nakiri.eventChannel.receiveBroadcastStream().listen((data) {
/// 这里的data就是原生端发送过来的数据
setState(() {
_eventValue = data;
});
}, onError: (error) { /// 错误处理
setState(() {
_eventValue = -5;
});
});
}
此外,还可以暂停接收数据、重新接收数据和移除接收数据:
/// 暂停数据接收
_stream?.pause();
/// 恢复数据流
_stream?.resume();
_removeStream() {
if (_stream != null) {
/// 移除监听
_stream?.cancel();
_stream = null;
}
}
移除监听后,想要重新监听时,只需要调用
_initStream()
方法即可,不需要重新创建eventChannel
的
插件上传到Pub
上传插件前,需要完善一些资料:
-
README.md
介绍包的文件 -
CHANGELOG.md
记录每个版本中的更改 -
LICENSE
包含软件包许可条款的文件 -
pubspec.yaml
的资料 - 所有公共API的API文档
首先是pubspec.yaml
,对Flutter
插件来说,pubspec.yaml
里除了插件的依赖,还包含一些元信息,根据需要,把这些补上:
name: xxx # 要发布的项目名称
description: xxxxxx. # 项目描述
version: 0.0.1 # 发布的版本
homepage: http://www.github.com/xxx # 项目主页
issue_tracker: http://www.github.com/xxx # issue,一般写当前插件源代码的Github issue地址
repository: http://www.github.com/xxx.git # 一般写当前插件源代码的Github地址
另外,发布到Pub
上的包需要包含一个LICENSE
许可条款文件,不想麻烦的话,可以在GitHub
创建仓库的时候选中一个。
发布前检查
我们打开命令行,跳转到pubspec.yaml
文件所在的目录,在命令行使用以下命令来测试发布:
flutter packages pub publish --dry-run --server=https://pub.dartlang.org
之所以使用
--server
来指定服务器,是因为我们在配置环境的时候,一般都配置了这2个变量:PUB_HOSTED_URL=https://pub.flutter-io.cn
和FLUTTER_STORAGE_BASE_URL=https://storage.flutter-io.cn
,直接上传会出现问题
如果没有发现问题,如图所示:
然后还需要做的就是上传前的需要清理插件,避免插件过大无法上传:
flutter clean
使用以下命令来发布插件:
flutter packages pub publish --server=https://pub.dartlang.org
因为是发布到谷歌的平台,所以需要登录谷歌账号进行认证。
在我们输入flutter packages pub publish
命令之后,我们会收到一条认证链接,使用浏览器打开链接就可以验证了。
我们选择自己的账户,即可开始验证,命令行会自行同步状态,无须我们自己处理的。
网页出现以下提示,就说明验证成功。
之后我们只需要等待即可,命令行会自行上传插件到Pub
的。
但是,如果遇到这种情况,说明是被墙了,需要使用代理。
特别说明下,在代理客户端上开启了代理,并不等于命令行就开启了代理,命令行需要额外开启,具体方法可自行查找。
上传成功后,会出现如下提示:
上传成功后,并不会马上能看到,请耐心等待。
插件的使用方式
插件有4种使用方式:
- pub
- git
- 本地
- 私有pub库
pub依赖
这种是最常见的方式,直接在工程的pubspec.yaml
中写上你需要的插件名和版本,之后执行Pub get
就行了。
dependencies:
flutter:
sdk: flutter
nakiri: ^0.0.1 # 添加库
git依赖
如果我们不想发布到pub
,但又想团队共享插件,那么我们可以把库上传到git仓库里面,然后在pubspec.yaml
中配置,之后执行Pub get
就行了。
dependencies:
flutter:
sdk: flutter
nakiri:
git:
url: https://github.com/xxx/nakiri.git
ref: nakiri_fixes_issue_520
path: packages/nakiri_2
-
url
:git地址 -
ref
:表示git引用,可以是commit hash
,tag
或者分支
-
path
:如果git仓库中有多个软件包,则可以使用此属性指定软件包
本地依赖
上面的方法都需要上传到服务器,较为麻烦,如果只是自己用或者调试插件,那么最好的方式就是本地依赖,只需要在pubspec.yaml
中配置路径,之后执行Pub get
就行了。
dependencies:
flutter:
sdk: flutter
nakiri:
path: ../xxx/nakiri/
path
可以是相对路径,也可以是绝对路径
私有pub仓库依赖
一般而言,pub
管理插件比git
管理方便,所以一般大公司都会搭建自己的私有pub仓库,依赖私有pub仓库也很简单,只需要在pubspec.yaml
中配置完成后,之后执行Pub get
就行了。
dependencies:
flutter:
sdk: flutter
nakiri:
hosted:
name: nakiri
url: http://your-package-server.com
version: ^0.0.1
依赖覆盖
当2个以上的插件依赖于另一个插件,并且他们所依赖的版本不一致的时候,就可能出现版本冲突,要解决这个冲突,我们可以使用dependency_overrides
强制某个插件使用某个版本,如:
dependencies:
nakiri_2: ^1.0.1
nakiri: ^0.0.1
dependency_overrides:
url_launcher: ^5.4.0
这里假设nakiri_2
和nakiri
都依赖url_launcher
,但所依赖的版本不一样,通过这种方式,可以让它们都依赖成5.4.0
版本。
虽然这种方式可以解决依赖报错,但可能会由于版本的改动使得API接口可能不一样,最终还是可能会出问题,所以,慎用。
相关链接
整个项目已经发到GitHub:demo地址
插件也已经发布到Pub仓库:插件地址
参考资料:Flutter中文网 - Flutter插件教程
iOS OC Swift Flutter开发群 139322447