Flutter混編工程之高速公路Pigeon
前面我們講到了Flutter與原生通信使用的是BasicMessageChannel,完全實現(xiàn)了接口解耦,通過協(xié)議來進行通信,但是這樣的一個問題是,多端都需要維護一套協(xié)議規(guī)范,這樣勢必會導(dǎo)致協(xié)作開發(fā)時的通信成本,所以,F(xiàn)lutter官方給出了Pigeon這樣一個解決方案。
Pigeon的存在就是為了解決多端通信的開發(fā)成本。其核心原理就是通過一套協(xié)議來生成多端的代碼,這樣多端只需要維護一套協(xié)議即可,其它代碼都可以通過Pigeon來自動生成,這樣就保證了多端的統(tǒng)一。
官方文檔如下所示。
https://pub.flutter-io.cn/packages/pigeon/install
引入
首先,需要dev_dependencies中引入Pigeon:
dev_dependencies:
pigeon: ^1.0.15
接下來,在Flutter的lib文件夾同級目錄下,創(chuàng)建一個.dart文件,例如schema.dart,這里就是通信的協(xié)議文件。
例如我們需要多端統(tǒng)一的一個實體:Book,如下所示。
import 'package:pigeon/pigeon.dart';
class Book {
String? title;
String? author;
}
@HostApi()
abstract class NativeBookApi {
List<Book?> getNativeBookSearch(String keyword);
void doMethodCall();
}
這就是我們的協(xié)議文件,其中@HostApi,代表從Flutter端調(diào)用原生側(cè)的方法,如果是@FlutterApi,那么則代表從原生側(cè)調(diào)用Flutter的方法。
生成
執(zhí)行下面的指令,就可以讓Pigeon根據(jù)協(xié)議來生成相應(yīng)的代碼,下面的這些配置,需要指定一些文件目錄和包名等信息,我們可以將它保存到一個sh文件中,這樣更新后,只需要執(zhí)行下這個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"
這里面比較重要的就是導(dǎo)入schema.dart文件,作為協(xié)議,再指定Dart、iOS和Android代碼的輸出路徑即可。
正常情況下,生成完后的代碼就可以直接使用了。
Pigeon生成的代碼是Java和OC,主要是為了能夠兼容更多的項目。你可以將它轉(zhuǎn)化為Kotlin或者Swift。
使用就以上面這個例子,我們來看下如何根據(jù)Pigeon生成的代碼來進行跨端通信。
首先,在Android代碼中,會生成一個同名協(xié)議的接口,NativeBookApi,對應(yīng)上面HostApi注解標記的協(xié)議名。在FlutterActivity的繼承類中,創(chuàng)建這個接口的實現(xiàn)類。
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的方式進行創(chuàng)建,如果是其它方式,按照不同的方法獲取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和協(xié)議的實現(xiàn)即可。
接下來,我們來看下如何在Flutter中調(diào)用這個方法,在有Pigeon之前,我們都是通過Channel,創(chuàng)建String類型的協(xié)議名來通信的,現(xiàn)在有了Pigeon之后,這些容易出錯的String就都被隱藏起來了,全部變成了正常的方法調(diào)用。
在Flutter中,Pigeon自動創(chuàng)建了NativeBookApi類,而不是Android中的接口,在類中已經(jīng)生成了getNativeBookSearch和doMethodCall這些協(xié)議中定義的方法。
List<Book?> list = await api.getNativeBookSearch("xxx");
setState(() => _counter = "${list[0]?.title} ${list[0]?.author}");
通過await就可以很方便的進行調(diào)用了。可見,通過Pigeon進行封裝后,跨端通信完全被協(xié)議所封裝了,同時也隱藏了各種String的處理,這樣就進一步降低了人工出錯的可能性。
優(yōu)化
在實際的使用中,F(xiàn)lutter調(diào)用原生方法來獲取數(shù)據(jù),原生側(cè)處理好數(shù)據(jù)后回傳給Flutter,所以在Pigeon生成的Android代碼中,協(xié)議函數(shù)的實現(xiàn)是一個帶返回值的方法,如下所示。
override fun getNativeBookSearch(keyword: String?): MutableList<Api.Book> {
val book = Api.Book().apply {
title = "android"
author = "xys$keyword"
}
return Collections.singletonList(book)
}
這個方法本身沒有什么問題,假如是網(wǎng)絡(luò)請求,可以使用OKHttp的success和fail回調(diào)來進行處理,但是,如果要使用協(xié)程呢?
由于協(xié)程破除了回調(diào),所以無法在Pigeon生成的函數(shù)中使用,這時候,就需要修改協(xié)議,給方法增加一個@async注解,將它標記為一個異步函數(shù)。
我們修改協(xié)議,并重新生成代碼。
@HostApi()
abstract class NativeBookApi {
@async
List<Book?> getNativeBookSearch(String keyword);
void doMethodCall();
}
這時候你會發(fā)現(xiàn),NativeBookApi的實現(xiàn)函數(shù)中,帶返回值的函數(shù)已經(jīng)變成了void,同時提供了一個result變量來處理返回值的傳遞。
override fun getNativeBookSearch(keyword: String?, result: Api.Result<MutableList<Api.Book>>?)
這樣使用就非常簡單了,將返回值通過result塞回去就好了。
有了這個方法,我們就可以將Pigeon和協(xié)程配合起來使用,開發(fā)體驗瞬間上升。
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))
}
}
協(xié)程+Pigeon YYDS。
這里只介紹了Flutter調(diào)用Android的場景,實際上Android調(diào)用Flutter也只是換了個方向而已,代碼都是類似的,這里不贅述了,那iOS呢?——我寫Flutter,關(guān)iOS什么事。
拆解
在了解了Pigeon如何使用之后,我們來看下,這只「鴿子」到底做了些什么。
從宏觀上來看,不管是Dart端還是Android端,都是生成了三類東西。
- 數(shù)據(jù)實體類,例如上面的Book類
- StandardMessageCodec,這是BasicMessageChannel的傳輸編碼類
- 協(xié)議接口\類,例如上面的NativeBookApi
在Dart中,數(shù)據(jù)實體會自動幫你生成encode和decode的代碼,這樣你獲取出來的數(shù)據(jù)就不再是Channel中的Object類型了,而是協(xié)議中定義的類型,極大的方便了開發(fā)者。
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的標準編解碼器,傳輸?shù)臄?shù)據(jù)需要實現(xiàn)它的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代碼幾乎一致,也很好理解,畢竟是一套協(xié)議,規(guī)則是一樣的。
下面就是Pigeon的核心了,我們來看具體的協(xié)議是如何實現(xiàn)的,首先來看下Dart中是如何實現(xiàn)的,由于我們是從Flutter中調(diào)用Android中的代碼,所以按照Channel的原理來說,我們需要在Dart中申明一個Channel,并處理其返回的數(shù)據(jù)。
如果你熟悉Channel的使用,那么這段代碼應(yīng)該是比較清晰的。
下面再來看看Android中的實現(xiàn)。Android側(cè)是事件的處理者,所以需要實現(xiàn)協(xié)議的具體內(nèi)容,這就是我們前面實現(xiàn)的接口,另外,還需要添加setMessageHandler來處理具體的協(xié)議。
這里有點意思的地方是那個Reply類的封裝。
public interface Result<T> {
void success(T result);
void error(Throwable error);
}
前面我們說了,在Pigeon中可以通過@async來生成異步接口,這個異步接口的實現(xiàn),實際上就是這里處理的。
看到這里,你應(yīng)該幾乎就了解了Pigeon到底是如何工作的了,說白了實際上就是通過build_runner來生成這些代碼,把臟活累活都自己吞下去了,我們看見的,實際上就是具體協(xié)議類的實現(xiàn)和調(diào)用。
題外話
所以說,Pigeon并不是什么非常高深的內(nèi)容,但卻是Flutter混編的一個非常重要的思想,或者說是Flutter團隊的一個指導(dǎo)思想,那就是通過「協(xié)議」「模板」來生成相關(guān)的代碼,類似的還有JSON解析的例子,實際上也是如此。
再講的多一點,Android模塊之間的解耦、模塊化操作,實際上是不是也能通過這種方式來處理呢?所以說,大道至簡,殊途同歸,軟件工程做到最后,實際上思想都是類似的,萬物斗轉(zhuǎn)星移,唯有思想永恒。