导语: 跨端开发中,经常会遇到插件,接口管理上的问题。了解完本文,你将会了解Flutter是如何通过Pigeon去解决plugin中多端开发难以管理的问题。
demo源码地址 https://github.com/linpenghui958/flutterPigeonDemo
warning:目前Pigeon还是prerelease版本,所以可能会有breaking change。下文以0.1.7版本为例。
为何需要Pigeon
在hybird开发中,前端需要native能力,需要native双端开发提供接口。这种情况下就如何规范命名,参数等就成了一个问题,如果单独维护一份协议文件,三端依照协议文件进行开发,很容易出现协议更改后,没有及时同步,又或者在实际开发过程没有按照规范,可能导致各种意外情况。在Flutter插件包的开发中,因为涉及到native双端代码实现能力,dart侧暴露统一的接口给使用者,也会出现同样的问题,这里Flutter官方推荐使用Pigeon进行插件管理。
Pigeon的作用
Flutter官方提供的Pigeon插件,通过dart入口,生成双端通用的模板代码,Native部分只需通过重写模板内的接口,无需关心methodChannel部分的具体实现,入参,出参也均通过生成的模板代码进行约束。假设接口新增,或者参数修改,只需要在dart侧更新协议文件,生成双端模板,即可达到同步更新。
以Flutter官方plugin中的video_player为例,接入pigeon后最终效果如下
可以看到接入pigeon后整体代码简洁了不少,而且规范了类型定义。接下来我们看一下如何从零接入Pigeon。
接入Pigeon
先看一下pub.dev上Pigeon的介绍,Pigeon只会生成Flutter与native平台通信所需的模板代码,没有其他运行时的要求,所以也不用担心Pigeon版本不同而导致的冲突。(这里的确不同版本使用起来差异较大,笔者这里接入的时候0.1.7与0.1.10,pigeon默认导出和使用都不相同)
创建package
ps:如果接入已有plugin库,可以跳过此部分,直接看接入部分。
执行生成插件包命令:
代码语言:javascript复制flutter create --org com.exmple --template plugin flutterPigeonDemo
要创建插件包,使用--template=plugin
参数执行flutter create
lib/flutter_pigeon_demo.dart
- 插件包的dart api
android/src/main/kotlin/com/example/flutter_pigeon_demo/FlutterPigeonPlugin.kt
- 插件包Android部分的实现
ios/Classes/FlutterPigeonDemoPlugin.m
- 插件包ios部分的实现。
example/
- 使用该插件的flutterdemo。
这里常规通过methodChannel实现plugin的部分省略,主要讲解一下如何接入pigeon插件。
添加依赖
首先在pubspec.yaml
中添加依赖
dev_dependencies:
flutter_test:
sdk: flutter
pigeon:
version: 0.1.7
然后按照官方的要求添加一个pigeons目录,这里我们放dart侧的入口文件,内容为接口、参数、返回值的定义,后面通过pigeon的命令,生产native端代码。
这里以pigeons/pigeonDemoMessage.dart
为例
import 'package:pigeon/pigeon.dart';
class DemoReply {
String result;
}
class DemoRequest {
String methodName;
}
// 需要实现的api
@HostApi()
abstract class PigeonDemoApi {
DemoReply getMessage(DemoRequest params);
}
// 输出配置
void configurePigeon(PigeonOptions opts) {
opts.dartOut = './lib/PigeonDemoMessage.dart';
opts.objcHeaderOut = 'ios/Classes/PigeonDemoMessage.h';
opts.objcSourceOut = 'ios/Classes/PigeonDemoMessage.m';
opts.objcOptions.prefix = 'FLT';
opts.javaOut =
'android/src/main/kotlin/com/example/flutter_pigeon_demo/PigeonDemoMessage.java';
opts.javaOptions.package = 'package com.example.flutter_pigeon_demo';
}
pigeonDemoMessage.dart
文件中定义了请求参数类型、返回值类型、通信的接口以及pigeon输出的配置。
这里@HostApi()
标注了通信对象和接口的定义,后续需要在native侧注册该对象,在Dart侧通过该对象的实例来调用接口。
configurePigeon
为执行pigeon生产双端模板代码的输出配置。
dartOut
为dart侧输出位置objcHeaderOut、objcSourceOut
为iOS侧输出位置prefix
为插件默认的前缀javaOut、javaOptions.package
为Android侧输出位置和包名
之后我们只需要执行如下命令,就可以生成对应的代码到指定目录中。
代码语言:javascript复制flutter pub run pigeon --input pigeons/pigeonDemoMessage.dart
--input
为我们的输入文件
生成模板代码后的项目目录如下
项目目录
我们在Plugin库中只需要管理标红的dart文件,其余标绿的则为通过Pigeon自动生成的模板代码。
我们接下来看一下双端如何使用Pigeon生成的模板文件。
Android端接入
这里Pigeon生产的PigeonDemoMessage.java
文件中,可以看到入参和出参的定义DemoRequest、DemoReply
,而PigeonDemoApi
接口,后面需要在plugin中继承PigeonDemoApi并实现对应的方法,其中setup函数用来注册对应方法所需的methodChannel。
ps: 这里生成的PigeonDemoApi部分,setup使用了接口中静态方法的默认实现,这里需要api level 24才能支持,这里需要注意一下。 考虑到兼容性问题,可以将setup的定义转移到plugin中。
首先需要在plugin文件中引入生成的PigeonDemoMessage中的接口和类。FlutterPigeonDemoPlugin先要继承PigeonDemoApi。然后在onAttachedToEngine中进行PigeonDemoApi的setup注册。并在plugin中重写PigeonDemoApi中定义的getMessage方法
伪代码部分
代码语言:javascript复制// ... 省略其他引入
import com.example.flutter_pigeon_demo.PigeonDemoMessage.*
// 继承PigeonDemoApi
public class FlutterPigeonDemoPlugin: FlutterPlugin, MethodCallHandler, PigeonDemoApi {
//...
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "flutter_pigeon_demo")
channel.setMethodCallHandler(this);
// pigeon生成的api进行初始化
PigeonDemoApi.setup(flutterPluginBinding.binaryMessenger, this);
}
// 重写PigeonDemoApi中的getMessage方法
override fun getMessage(arg: DemoRequest): DemoReply {
var reply = DemoReply();
reply.result = "pigeon demo result";
return reply;
}
}
iOS接入
ios相关目录下的PigeonDemoMessage.m
也有FLTDemoReply、FLTDemoRequest、FLTPigeonDemoApiSetup
的实现。首先需要在plugin中引入头文件PigeonDemoMessage.h
,需要在registerWithRegistrar中注册setup函数,并实现getMessage方法。
#import "FlutterPigeonDemoPlugin.h"
#import "PigeonDemoMessage.h"
@implementation FlutterPigeonDemoPlugin
(void)registerWithRegistrar:(NSObject<FlutterPluginRegistrar>*)registrar {
FlutterPigeonDemoPlugin* instance = [[FlutterPigeonDemoPlugin alloc] init];
// 注册api
FLTPigeonDemoApiSetup(registrar.messenger, instance);
}
// 重写getMessage方法
- (FLTDemoReply*)getMessage:(FLTDemoRequest*)input error:(FlutterError**)error {
FLTDemoReply* reply = [[FLTDemoReply alloc] init];
reply.result = @"pigeon demo result";
return reply;
}
@end
Dart侧使用
最终在dart侧如何调用呢 首先看一下lib下Pigeon生成的dart文件PigeonDemoMessage.dartDemoReply、DemoRequest
用来实例化入参和出参 然后通过PigeonDemoApi
的实例去调用方法。
import 'dart:async';
import 'package:flutter/services.dart';
import 'PigeonDemoMessage.dart';
class FlutterPigeonDemo {
static const MethodChannel _channel =
const MethodChannel('flutter_pigeon_demo');
static Future<String> get platformVersion async {
final String version = await _channel.invokeMethod('getPlatformVersion');
return version;
}
static Future<DemoReply> testPigeon() async {
// 初始化请求参数
DemoRequest requestParams = DemoRequest()..methodName = 'requestMessage';
// 通过PigeonDemoApi实例去调用方法
PigeonDemoApi api = PigeonDemoApi();
DemoReply reply = await api.getMessage(requestParams);
return reply;
}
}
至此,Pigeon的接入就已经完成了。
接入Pigeon后的效果
本文demo代码较为简单,接入Pigeon前后的差异并不明显,我们可以看下一Flutter官方plugin中的video_player接入前后的对比。
左侧为接入Pigeon前,处理逻辑都在onMethodCall中,不同的方法通过传入的call.method来区分,代码复杂后很容易变成面条式代码,而且返回的参数也没有约定,有较多不确定因素。
右侧接入Pigeon后,只需要重写对应的方法,逻辑分离,直接通过函数名区分,只需要关心具体的业务逻辑即可。
而在dart的调用侧,接入前都是通过invokeMethod调用,传入的参数map内也是dynamic类型的值。接入后直接调用api的实例对象上的方法,并且通过Pigeon生成的模板代码,直接实例化参数对象。
总结:通过Pigeon来管理Flutter的plugin库,只需要在dart侧维护一份协议即可,即使在多端协同开发的情况下,也能达到约束和规范的作用。
在实现原生插件时我们可以省去很多重复代码,并且不需要关心具体methodchannel的name,也避免了常规情况下,可能出现的面条式代码,只需通过重写pigeon暴露的方法就可以完成双端的通信。而dart侧也只需要通过模板暴露的实例对象来调用接口方法。
源码分析
使用的时候,我们只知道运行命令flutter pub run pigeon --input xxx
就可以生成双端模板代码,接下来我们深入了解一下,这其中Pigeon到底做了什么。
首先,看一下plugin库默认导出的pigeon_lib.dart入口文件,这里主要有几个定义PigeonOptions、ParseResults、Pigeon。
- PigeonOptions,是执行命令生成模板时的选项。
- ParseResults,表示解析的结果集合包含了AST对象root,和解析过程产生的错误信息集合erros。
- Pigeon,是实际进行代码生成的类。
其中Pigeon的入口为run方法,这里进行了模板代码的生成。
run函数的入参是一个String类型的List,这里对应的是通过命令行输入的,PigeonOptions的选项。
函数开始先实例化了pigeon对象,并对传入的options进行解析生成编译所需的PigeonOptions。
这里提供了两种方式,一种是通过命令直接传入,一种是通过入口文件内configurePigeon的定义传入。
代码语言:javascript复制// Pigeon实例初始化
final Pigeon pigeon = Pigeon.setup();
// 解析命令行穿传入的参数
final PigeonOptions options = Pigeon.parseArgs(args);
// 解析入口文件内的参数
_executeConfigurePigeon(options);
//校验input(输入文件)或者dartOut(dart输出路径)是否为空
if (options.input == null || options.dartOut == null) {
print(usage);
return 0;
}
接下来会对objcHeaderOut、javaOut为空的情况取默认值处理。
代码语言:javascript复制// 解析apis
final ParseResults parseResults = pigeon.parse(apis);
for (Error err in parseResults.errors) {
errors.add(Error(message: err.message, filename: options.input));
}
这里parse解析生成的parseResults对象,最终用parseResults中的ast对象root来生成多端模板代码。
这里首先将需要实现的api类和参数类进行了区分。(ps:这里_isApi中便是通过dart入口中@HostApi注解进行区分)
代码语言:javascript复制for (Type type in types) {
final ClassMirror classMirror = reflectClass(type);
if (_isApi(classMirror)) {
apis.add(classMirror);
} else {
classes.add(classMirror);
}
}
然后对参数类型进行区分,并给root对象添加了classes和apis属性。
这里classes对应模板中参数的类。而apis则对应模板中含有函数的方法类。
代码语言:javascript复制root.classes =
_unique(_parseClassMirrors(classes), (Class x) => x.name).toList();
root.apis = <Api>[];
for (ClassMirror apiMirror in apis) {
final List<Method> functions = <Method>[];
for (DeclarationMirror declaration in apiMirror.declarations.values) {
if (declaration is MethodMirror && !declaration.isConstructor) {
// 省略处理过程
}
}
final HostApi hostApi = _getHostApi(apiMirror);
root.apis.add(Api(
name: MirrorSystem.getName(apiMirror.simpleName),
location: hostApi != null ? ApiLocation.host : ApiLocation.flutter,
methods: functions,
dartHostTestHandler: hostApi?.dartHostTestHandler));
}
最后根据解析后的root对象,来生成对应各端的代码。
代码语言:javascript复制if (options.dartOut != null) {
await _runGenerator(
options.dartOut,
(StringSink sink) =>
generateDart(options.dartOptions, parseResults.root, sink));
}
if (options.objcHeaderOut != null) {
await _runGenerator(
options.objcHeaderOut,
(StringSink sink) => generateObjcHeader(
options.objcOptions, parseResults.root, sink));
}
if (options.objcSourceOut != null) {
await _runGenerator(
options.objcSourceOut,
(StringSink sink) => generateObjcSource(
options.objcOptions, parseResults.root, sink));
}
if (options.javaOut != null) {
await _runGenerator(
options.javaOut,
(StringSink sink) =>
generateJava(options.javaOptions, parseResults.root, sink));
}
这里每个具体生成输出的函数就比较简单,这里以dart端的generateDart函数为例,通过root对象,遍历其中的class和api来生成对应的模板代码,这里模板都是已经预先定义好的。如果项目本身有定制化输出模板的需求,只需要修改对应的部分就好了。
代码语言:javascript复制void generateDart(DartOptions opt, Root root, StringSink sink) {
final List<String> customClassNames =
root.classes.map((Class x) => x.name).toList();
final Indent indent = Indent(sink);
indent.writeln('// $generatedCodeWarning');
indent.writeln('// $seeAlsoWarning');
indent.writeln(
'// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import');
indent.writeln('// @dart = ${opt.isNullSafe ? '2.10' : '2.8'}');
indent.writeln('import 'dart:async';');
indent.writeln('import 'package:flutter/services.dart';');
indent.writeln(
'import 'dart:typed_data' show Uint8List, Int32List, Int64List, Float64List;');
indent.writeln('');
final String nullBang = opt.isNullSafe ? '!' : '';
// 遍历输出参数类
for (Class klass in root.classes) {
sink.write('class ${klass.name} ');
indent.scoped('{', '}', () {
for (Field field in klass.fields) {
final String datatype =
opt.isNullSafe ? '${field.dataType}?' : field.dataType;
indent.writeln('$datatype ${field.name};');
}
indent.writeln('// ignore: unused_element');
indent.write('Map<dynamic, dynamic> _toMap() ');
indent.scoped('{', '}', () {
indent.writeln(
'final Map<dynamic, dynamic> pigeonMap = <dynamic, dynamic>{};');
for (Field field in klass.fields) {
indent.write('pigeonMap['${field.name}'] = ');
if (customClassNames.contains(field.dataType)) {
indent.addln(
'${field.name} == null ? null : ${field.name}$nullBang._toMap();');
} else {
indent.addln('${field.name};');
}
}
indent.writeln('return pigeonMap;');
});
indent.writeln('// ignore: unused_element');
indent.write(
'static ${klass.name} _fromMap(Map<dynamic, dynamic> pigeonMap) ');
indent.scoped('{', '}', () {
indent.writeln('final ${klass.name} result = ${klass.name}();');
for (Field field in klass.fields) {
indent.write('result.${field.name} = ');
if (customClassNames.contains(field.dataType)) {
indent.addln(
'pigeonMap['${field.name}'] != null ? ${field.dataType}._fromMap(pigeonMap['${field.name}']) : null;');
} else {
indent.addln('pigeonMap['${field.name}'];');
}
}
indent.writeln('return result;');
});
});
indent.writeln('');
}
// 省略apis接口部分的输出
}
腾讯音乐QQ音乐/全民K歌招聘客户端、web前端、后台开发,点击查看原文投递简历!或邮箱联系: godjliu@tencent.com