導語: 跨端開發中,經常會遇到插件,接口管理上的問題。了解完本文,你将會了解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庫,可以跳過此部分,直接看接入部分。
執行生成插件包指令:
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生産雙端模闆代碼的輸出配置。
-
為dart側輸出位置dartOut
-
為iOS側輸出位置objcHeaderOut、objcSourceOut
-
為插件預設的字首prefix
-
為Android側輸出位置和包名javaOut、javaOptions.package
之後我們隻需要執行如下指令,就可以生成對應的代碼到指定目錄中。
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方法
僞代碼部分
// ... 省略其他引入
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的定義傳入。
// 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為空的情況取預設值處理。
// 解析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注解進行區分)
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則對應模闆中含有函數的方法類。
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對象,來生成對應各端的代碼。
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來生成對應的模闆代碼,這裡模闆都是已經預先定義好的。如果項目本身有定制化輸出模闆的需求,隻需要修改對應的部分就好了。
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前端、背景開發,點選檢視原文投遞履歷!或郵箱聯系: [email protected]