Flutter混編工程之高速公路Pigeon
前面我們講到了Flutter與原生通信使用的是BasicMessageChannel,完全實現了接口解耦,通過協議來進行通信,但是這樣的一個問題是,多端都需要維護一套協議規范,這樣勢必會導致協作開發時的通信成本,所以,Flutter官方給出了Pigeon這樣一個解決方案。
Pigeon的存在就是為了解決多端通信的開發成本。其核心原理就是通過一套協議來生成多端的代碼,這樣多端只需要維護一套協議即可,其它代碼都可以通過Pigeon來自動生成,這樣就保證了多端的統一。
官方文檔如下所示。
https://pub.flutter-io.cn/packages/pigeon/install
引入
首先,需要dev_dependencies中引入Pigeon:
dev_dependencies:
pigeon: ^1.0.15
接下來,在Flutter的lib文件夾同級目錄下,創建一個.dart文件,例如schema.dart,這里就是通信的協議文件。
例如我們需要多端統一的一個實體:Book,如下所示。
import 'package:pigeon/pigeon.dart';
class Book {
String? title;
String? author;
}
@HostApi()
abstract class NativeBookApi {
List<Book?> getNativeBookSearch(String keyword);
void doMethodCall();
}
這就是我們的協議文件,其中@HostApi,代表從Flutter端調用原生側的方法,如果是@FlutterApi,那么則代表從原生側調用Flutter的方法。
生成
執行下面的指令,就可以讓Pigeon根據協議來生成相應的代碼,下面的這些配置,需要指定一些文件目錄和包名等信息,我們可以將它保存到一個sh文件中,這樣更新后,只需要執行下這個sh文件即可。
flutter pub run pigeon \
--input schema.dart \
--dart_out lib/pigeon.dart \
--objc_header_out ios/Runner/pigeon.h \
--objc_source_out ios/Runner/pigeon.m \
--java_out ./android/app/src/main/java/dev/flutter/pigeon/Pigeon.java \
--java_package "dev.flutter.pigeon"
這里面比較重要的就是導入schema.dart文件,作為協議,再指定Dart、iOS和Android代碼的輸出路徑即可。
正常情況下,生成完后的代碼就可以直接使用了。
Pigeon生成的代碼是Java和OC,主要是為了能夠兼容更多的項目。你可以將它轉化為Kotlin或者Swift。
使用就以上面這個例子,我們來看下如何根據Pigeon生成的代碼來進行跨端通信。
首先,在Android代碼中,會生成一個同名協議的接口,NativeBookApi,對應上面HostApi注解標記的協議名。在FlutterActivity的繼承類中,創建這個接口的實現類。
private class NativeBookApiImp(val context: Context) : Api.NativeBookApi {
override fun getNativeBookSearch(keyword: String?): MutableList<Api.Book> {
val book = Api.Book().apply {
title = "android"
author = "xys$keyword"
}
return Collections.singletonList(book)
}
override fun doMethodCall() {
context.startActivity(Intent(context, FlutterMainActivity::class.java))
}
}
這里順便提一下,engine使用FlutterEngineGroup的方式進行創建,如果是其它方式,按照不同的方法獲取engine對象即可。
class SingleFlutterActivity : FlutterActivity() {
val engine: FlutterEngine by lazy {
val app = activity.applicationContext as QDApplication
val dartEntrypoint =
DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(), "main"
)
app.engines.createAndRunEngine(activity, dartEntrypoint)
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
Api.NativeBookApi.setup(flutterEngine.dartExecutor, NativeBookApiImp(this))
}
override fun provideFlutterEngine(context: Context): FlutterEngine? {
return engine
}
override fun onDestroy() {
super.onDestroy()
engine.destroy()
}
}
初始化Pigeon的核心方法就是NativeBookApi中的setup方法,傳入engine和協議的實現即可。
接下來,我們來看下如何在Flutter中調用這個方法,在有Pigeon之前,我們都是通過Channel,創建String類型的協議名來通信的,現在有了Pigeon之后,這些容易出錯的String就都被隱藏起來了,全部變成了正常的方法調用。
在Flutter中,Pigeon自動創建了NativeBookApi類,而不是Android中的接口,在類中已經生成了getNativeBookSearch和doMethodCall這些協議中定義的方法。
List<Book?> list = await api.getNativeBookSearch("xxx");
setState(() => _counter = "${list[0]?.title} ${list[0]?.author}");
通過await就可以很方便的進行調用了。可見,通過Pigeon進行封裝后,跨端通信完全被協議所封裝了,同時也隱藏了各種String的處理,這樣就進一步降低了人工出錯的可能性。
優化
在實際的使用中,Flutter調用原生方法來獲取數據,原生側處理好數據后回傳給Flutter,所以在Pigeon生成的Android代碼中,協議函數的實現是一個帶返回值的方法,如下所示。
override fun getNativeBookSearch(keyword: String?): MutableList<Api.Book> {
val book = Api.Book().apply {
title = "android"
author = "xys$keyword"
}
return Collections.singletonList(book)
}
這個方法本身沒有什么問題,假如是網絡請求,可以使用OKHttp的success和fail回調來進行處理,但是,如果要使用協程呢?
由于協程破除了回調,所以無法在Pigeon生成的函數中使用,這時候,就需要修改協議,給方法增加一個@async注解,將它標記為一個異步函數。
我們修改協議,并重新生成代碼。
@HostApi()
abstract class NativeBookApi {
@async
List<Book?> getNativeBookSearch(String keyword);
void doMethodCall();
}
這時候你會發現,NativeBookApi的實現函數中,帶返回值的函數已經變成了void,同時提供了一個result變量來處理返回值的傳遞。
override fun getNativeBookSearch(keyword: String?, result: Api.Result<MutableList<Api.Book>>?)
這樣使用就非常簡單了,將返回值通過result塞回去就好了。
有了這個方法,我們就可以將Pigeon和協程配合起來使用,開發體驗瞬間上升。
private class NativeBookApiImp(val context: Context, val lifecycleScope: LifecycleCoroutineScope) : Api.NativeBookApi {
override fun getNativeBookSearch(keyword: String?, result: Api.Result<MutableList<Api.Book>>?) {
lifecycleScope.launch {
try {
val data = RetrofitClient.getCommonApi().getXXXXList().data
val book = Api.Book().apply {
title = data.tagList.toString()
author = "xys$keyword"
}
result?.success(Collections.singletonList(book))
} catch (e: Exception) {
e.printStackTrace()
}
}
}
override fun doMethodCall() {
context.startActivity(Intent(context, FlutterMainActivity::class.java))
}
}
協程+Pigeon YYDS。
這里只介紹了Flutter調用Android的場景,實際上Android調用Flutter也只是換了個方向而已,代碼都是類似的,這里不贅述了,那iOS呢?——我寫Flutter,關iOS什么事。
拆解
在了解了Pigeon如何使用之后,我們來看下,這只「鴿子」到底做了些什么。
從宏觀上來看,不管是Dart端還是Android端,都是生成了三類東西。
- 數據實體類,例如上面的Book類
- StandardMessageCodec,這是BasicMessageChannel的傳輸編碼類
- 協議接口\類,例如上面的NativeBookApi
在Dart中,數據實體會自動幫你生成encode和decode的代碼,這樣你獲取出來的數據就不再是Channel中的Object類型了,而是協議中定義的類型,極大的方便了開發者。
class Book {
String? title;
String? author;
Object encode() {
final Map<Object?, Object?> pigeonMap = <Object?, Object?>{};
pigeonMap['title'] = title;
pigeonMap['author'] = author;
return pigeonMap;
}
static Book decode(Object message) {
final Map<Object?, Object?> pigeonMap = message as Map<Object?, Object?>;
return Book()
..title = pigeonMap['title'] as String?
..author = pigeonMap['author'] as String?;
}
}
在Android中,也是做的類似的操作,可以理解為用Java翻譯了一遍。
下面是Codec,StandardMessageCodec是BasicMessageChannel的標準編解碼器,傳輸的數據需要實現它的writeValue和readValueOfType方法。
class _NativeBookApiCodec extends StandardMessageCodec {
const _NativeBookApiCodec();
@override
void writeValue(WriteBuffer buffer, Object? value) {
if (value is Book) {
buffer.putUint8(128);
writeValue(buffer, value.encode());
} else {
super.writeValue(buffer, value);
}
}
@override
Object? readValueOfType(int type, ReadBuffer buffer) {
switch (type) {
case 128:
return Book.decode(readValue(buffer)!);
default:
return super.readValueOfType(type, buffer);
}
}
}
同樣的,Dart和Android代碼幾乎一致,也很好理解,畢竟是一套協議,規則是一樣的。
下面就是Pigeon的核心了,我們來看具體的協議是如何實現的,首先來看下Dart中是如何實現的,由于我們是從Flutter中調用Android中的代碼,所以按照Channel的原理來說,我們需要在Dart中申明一個Channel,并處理其返回的數據。
如果你熟悉Channel的使用,那么這段代碼應該是比較清晰的。
下面再來看看Android中的實現。Android側是事件的處理者,所以需要實現協議的具體內容,這就是我們前面實現的接口,另外,還需要添加setMessageHandler來處理具體的協議。
這里有點意思的地方是那個Reply類的封裝。
public interface Result<T> {
void success(T result);
void error(Throwable error);
}
前面我們說了,在Pigeon中可以通過@async來生成異步接口,這個異步接口的實現,實際上就是這里處理的。
看到這里,你應該幾乎就了解了Pigeon到底是如何工作的了,說白了實際上就是通過build_runner來生成這些代碼,把臟活累活都自己吞下去了,我們看見的,實際上就是具體協議類的實現和調用。
題外話
所以說,Pigeon并不是什么非常高深的內容,但卻是Flutter混編的一個非常重要的思想,或者說是Flutter團隊的一個指導思想,那就是通過「協議」「模板」來生成相關的代碼,類似的還有JSON解析的例子,實際上也是如此。
再講的多一點,Android模塊之間的解耦、模塊化操作,實際上是不是也能通過這種方式來處理呢?所以說,大道至簡,殊途同歸,軟件工程做到最后,實際上思想都是類似的,萬物斗轉星移,唯有思想永恒。