实现一个简单的 Flutter 插件
本文最后更新于 2021年4月4日 晚上
记录一次 Flutter 插件的实现, 目前仅实现了 iOS 端.
实现
插件调用原生 API, 需要 Pods 支持, 开发流程如下.
整体流程
使用 IDE 或命令行新建工程:
1
flutter create --org com.example --template=plugin 包名
若想指定开发语言, 则需要:
1
flutter create --org com.example --template=plugin -i swift -a kotlin 包名
插件包 API 在 lib 中实现.
针对 iOS 平台, 需要首先在 example 中执行如下命令:
1
flutter build ios --no-codesign
通过平台通道来实现 dart 和平台间的交互.
Flutter 作为 UI 层和平台进行交互时, 使用的是 MethodChannel 机制.
其实就是一套消息发送机制, 消息的发送和接收都是异步的, 从而保证不会阻塞 UI 线程.
在 Flutter 端, 使用 MethodChannel 向平台发送消息.
在平台端, 通过 MethodChannel(Android) 或 FlutterMethodChannel(iOS) 来接收消息并回传结果.
并且平台端也可以反向地和 Flutter 端进行联系, 从而实现一套 Flutter 作为后端, 而平台作为前端的结构.(具体做法可以参考 quick actions 这个插件.)
具体平台上数据类型支持可以参考这个链接.
Flutter 和平台间交互的实现可以看这个链接.
包支持三种方式的使用:
发布到 Dart 包网站再通过包管理来引入使用.
相对路径或绝对路径的方式:
1
2
3dependencies:
plugin1:
path: ../plugin1/git URL 的方式:
1
2
3
4
5dependencies:
package1:
git:
url: git://github.com/flutter/packages.git
path: packages/package1
实践
建立一个 flutter_plugin_demo 的工程, 这里直接使用 IDE, 因为还会进行依赖的获取, 故第一次耗时较长.
可以看到, 在 lib 中已经生成了部分可供使用的代码了, 其中使用
MethodChannel
类来实例化了一个消息通道对象, 通过这个对象的invokeMethod
方法就可以向平台端发送消息:1
final String str = await _channel.invokeMethod('消息名称', [1, 2, 'a']);
其中第一个参数是消息名称.
另外消息通道需要用到编解码器(codec), 默认情况下使用的是标准编解码器(另外还提供了一个 JSON 形式的编解码器, 可以将消息名和参数对应解析, 估计是用在网页端的).
定义插件中的 Flutter 层 API: 比如要提供平台上的电量信息.
设置消息通道名为:
top.rayy.demos/flutter_plugin_demo
, 这样的写法只是一种格式约定, 并没有实际功能上的含义, 接收端初始化消息通道时使用相同字符串即可.1
2
3
4class FlutterPluginDemoA {
static const MethodChannel _channel =
const MethodChannel('top.rayy.demos/flutter_plugin_demo');
}设置异步 API, 向平台端发送消息以获取电量信息:
1
2
3
4
5
6
7
8static Future<String> getBatteryLevel() async {
try {
int value = await _channel.invokeMethod('getBatteryLevel');
return '$value';
} catch (e) {
return 'unknown';
}
}发送消息时有三种可能的情况:
- 成功收到结果.
- 收到平台异常:
PlatformException
- 收到消息未实现(对应平台没有实现这个消息的处理)异常:
MissingPluginException
故需要使用
try-catch
来包裹消息发送的调用.
在特定平台上实现:
和普通的消息通道使用不同, 插件实现时, 在 iOS 端对应的处理类上实现了
FlutterPlugin
协议, 故不需要显式实例化消息通道, 而是在协议规定的类方法registerWithRegistrar
中处理消息通道的实例化, 并且在 对象方法handleMethodCall
中处理消息接收和结果发送.上述两个协议方法实现如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17+ (void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterMethodChannel* channel = [FlutterMethodChannel
methodChannelWithName:@"top.rayy.demos/flutter_plugin_demo"
binaryMessenger:[registrar messenger]];
FlutterPluginDemo_aPlugin* instance = [[FlutterPluginDemo_aPlugin alloc] init];
[registrar addMethodCallDelegate:instance channel:channel];
}
- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"getBatteryLevel" isEqualToString:call.method]) {
// TODO: 消息具体的处理代码.
} else {
result(FlutterMethodNotImplemented);
}
}在 Xcode 中保证编译正常后继续.
实现具体处理代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20- (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
if ([@"getPlatformVersion" isEqualToString:call.method]) {
result([@"iOS " stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
} else if ([@"getBatteryLevel" isEqualToString:call.method]) {
int level = [self batteryLevel];
result([NSNumber numberWithInt:level]);
} else {
result(FlutterMethodNotImplemented);
}
}
- (int)batteryLevel {
UIDevice *device = UIDevice.currentDevice;
device.batteryMonitoringEnabled = YES;
if (device.batteryState == UIDeviceBatteryStateUnknown) {
return -1;
} else {
return (int)(device.batteryLevel * 100);
}
}在 example 中进行使用测试:
因为要从异步转到同步, 在 Flutter 中利用了一个技巧, 即在某个方法中异步获取到值后进行
setState
操作来更新对应属性, 从而更新界面.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15Future<void> initPlatformState() async {
String batteryLevel;
try {
batteryLevel = await FlutterPluginDemoA.getBatteryLevel();
} on PlatformException {
// TODO: 异常处理
}
// 判断当前状态对象是否仍然和 Widget 关联, 如果没关联的话, setState 会崩溃.
if (!mounted) return;
setState( () {
_batteryLevel = batteryLevel;
});
}剩下的就是尝试在界面上展示出来了.
使用 git 提供给其他 dart 工程使用:
上传到一个可用的 git 仓库, 而后,
尝试:
1
2
3
4
5
6
7dependencies:
package1:
git:
# 替换为库的 URL
url: git://github.com/flutter/packages.git
# 如果有多个包, 需要替换为具体路径(比如 Flutter 团队提供的就是若干插件在一块的)
# path: packages/package1这里上传到的是 github 上面.
证明可以直接使用.
在插件中使用 Swift
严格来说这个不应算到 Flutter 这里, 而是 iOS 端的一个实践而已.
只需要在 OC 实现文件中引用 swift 生成的头文件就可以了…
不过注意, 引入头文件的时候需要使用如下语法:
1 |
|
另外 Swift 类需要是 public
的且标记为 @objc
才能在 OC 中访问到, 由于标记为 @objc
, 故还有一个隐性要求就是 class
, 且 class 必须继承自 NSObject
.
但是要注意, 默认情况下, Flutter 插件生成命令生成的是静态库类型的 pod, 而使用 swift 的话默认是动态库类型的 Framework.
且暂时没有找到在 OC 静态库中混编 OC 和 Swift 的方法.
故目前如果想要在插件中自己编写 swift 代码的话, 最好创建 Swift 版本的插件, 而如果只用 OC 的话, 则可以通过 Pod 引入三方 Swift 库作为这个库的平台端依赖使用.
插件中的 Pod 库分析
首先是定义的 Podfile 文件, 看懂这个后再继续. 这个文件中主要是在为 iOS 平台 example 提供 Flutter 依赖和自定义 Pod 库依赖. pod 可以认出这个 pod 是开发 pod, 使用下面的代码放到 Podfile 中即可将文件放到顶层, 方便开发:
1 |
|
总结
插件开发的时候最好使用 swift 模板, 方便起步, 另外替换上述 Podfile 方便开发. 不过有个蛋疼的地方就是移动文件的时候无法放到指定的文件夹下.
另外可以看看 CocoaPod 的文档, 因为其他相关的问题都是由于 CocoaPod 上的配置问题引起的.
Flutter和平台间的交互
交互的时候在 Flutter 端是这个样的:
- 用 method channel 发送消息
- 用 event channel 接收消息
平台端正好相反:
- 使用 method channel 接收消息
- 使用 eventSink 发送消息
从平台向 Flutter 发送/接收消息的流程如下所示:
之前已经搞了从 Flutter 向平台发送消息的流程, 现在来看从平台往 Flutter 发送消息的流程
Flutter 端
建立
EventChannel
:1
2static const EventChannel eventChannel =
EventChannel('samples.flutter.io/charging');在合适的位置启动监听 EventChannel 并提供事件处理方法:
1
2
3
4
5
6
7
8
9
10
11
12
13@override
void initState() {
super.initState();
eventChannel.receiveBroadcastStream().listen(_onEvent, onError: _onError);
}
void _onEvent(Object event) {
// ...
}
void _onError(Object error) {
// ...
}其中 Object event 是可以进行编解码的数据类型, 它代表的是平台端传入的数据.
平台端
选择一个合适的类, 并在其中建立事件通道:
1
2let chargingChannel = FlutterEventChannel(name: "samples.flutter.io/charging",
binaryMessenger: controller)在这个类中声明一个事件槽, 同时让该类实现
FlutterStreamHandler
协议:1
2
3class SomeClass: NSObject, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
}之后拿到事件槽的实例后, 就可以通过该实例向 Flutter 端发送消息.
将该类设置为事件通道的 streamHandler:
1
chargingChannel.setStreamHandler(self)
实现协议方法
onListenWithArguments
和onCancelWithArguments
:1
2
3
4
5
6
7
8
9
10public func onListen(withArguments arguments: Any?,
eventSink: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = eventSink
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
eventSink = nil
return nil
}通过事件槽发送消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16private func sendBatteryStateEvent() {
guard let eventSink = eventSink else { return }
switch UIDevice.current.batteryState {
case .full:
eventSink("charging")
case .charging:
eventSink("charging")
case .unplugged:
eventSink("discharging")
default:
eventSink(FlutterError(code: "unavailable",
message: "Charging status unavailable",
details: nil))
}
}此时如果 Flutter 端想要接收消息, 则只需要在 onEvent 中写如下代码:
1
2
3
4
5
6
7
8
9
10
11
12void _onEvent(Object event) {
if (!mounted) { return; }
String status;
if (event == 'charging') {
status = '正在充电';
} else {
status = '没有充电';
}
setState(() {
_chargingStatus = '充电状态: $status';
});
}
typedef 的语法好弹弹堂:
1 |
|
具体的实现已经放到了这个库中, 其中:
通过电量数据的获取来展示 Flutter —> 平台 的消息发送和结果处理.
通过监听网络状态变化来展示 平台 —> Flutter 的事件发送和处理.