在實際的作業場景中,我們很難從零開始用純Flutter去建設一個專案,也正是因為這樣,Native+Flutter混合堆疊跳轉管理使我們在混合開發的時候不得不首先考慮的問題,因為我們很難保證不會遇到下面的情況,

那么如何做技術選型困惑了不少想要做混開的同學,畢竟Flutter的生態還不是十分成熟,現成的解決方案和輪子并不多,而且還不一定好用,要么資源占用過高,要么侵入性太強,好在經過這幾天的摸索,總結出來了一套方案,供大家學習參考,相互交流,
按照國際慣例,我先介紹一下目前市場上的一些解決方案以及存在的問題,
本文章基于Flutter版本:2.2 & Platform:Android
1.Google官方(多引擎方案)
即每次使用一個新的FlutterEngine來渲染Widget樹,雖然Flutter 2.0之后的創建FlutterEngine的開銷大大降低,但是依然沒有解決每個FlutterEngine是一個單獨isolate,如果需要Flutter①和Flutter②之間互動資料的話,將會非常麻煩,我們同樣無法保證他們之間不會進行資料互動,因此Pass,
2.大名鼎鼎的閑魚flutter_boost(單引擎方案)
flutter_boost最近發布了3.0的bate版本,摒棄了2.0版本對引擎的侵入(贊!),但是依然存在不少問題:
①:高居不下的未關閉issues與回復不及時之間的矛盾(可以理解,竟然還是有作業要做的),
②:復雜的設計,在出現使用問題的時候,通過改flutter_boost原始碼解決的成本很高,
③:flutter方面耦合度較高,我必須使用flutter_boost提供的一系列Route相關的工具和Widget才能達到混合堆疊跳轉的效果,如果我以后想要更換框架,那對代碼的修改將是海量的,
于是乎成功把我勸退,
3.哈嘍單車團隊的flutter_thrio(單引擎方案)
該庫的優劣作者已經說得很詳細了,這里就不再贅述,感興趣的朋友可以進傳送門親自查看,
4.位元組跳動團隊的Isolate復用方案和騰訊心悅團隊的TRouter方案
很可惜,目前這兩個方案并沒有開源出來,但很可能位元組團隊的方案的侵入性相當高,
既然沒有現成的方案,那就擼起袖子造一個,先來一個混合堆疊跳轉的效果演示:
專案地址:github.com/wangkunhui/…

那么現在開始把上面的功能實作吧,
首先,要確定最終實作的目標,然后一步步朝著目標去完善:
目標一:復用FlutterEngine,避免額外的資源開銷與FlutterEngine之間的通信成本,
目標二:Flutter Widget之間的跳轉無需通過Native層控制(flutter_boost需要),
目標三:每個打開的Flutter能且只能管理自己內部的堆疊,當前Flutter的Widget全部出堆疊后,退出當前Flutter,
目標四:支持Flutter帶引數打開Native頁面(回傳值也可以支持,但目前還未添加進去),
根據這些目標設計出來的模型圖如下:

上圖是依次交叉打開的5個Activity的堆疊資訊示意圖,可以分為三列,具體解釋如下:
左側的是Activity的堆疊資訊,其中第二個和第四個掛載了Flutter,又分別打開了幾個Flutter的Widget,
中間是FlutterEngine中的Widget堆疊示意圖,因為復用FlutterEngine的原因,Widget A到Widget P都存在于一個堆疊中,而處在最底部的是一個空白的HostWidget,不負責任何業務邏輯,其目的就是保證其上方的業務相關的Widget能正常被Pop出堆疊(因為Flutter Navigator的最后一個Widget是無法被Pop的),
右側是對中間的FlutterEngine中Widget堆疊的管線資訊說明,就是那些Widget是關聯在那些HostActivity實體上的,為的就是能夠控制我對Widget進行退堆疊操作的時候,知道退到哪個Widget時需要finish掉關聯的Activity,
如果上面的描述不是很直觀的話,那我舉個栗子說明,我在HostActivity 實體2里打開兩個Flutter頁面,Widget O和Widget P,當這兩個Widget完成自己的任務后,就要進行退出操作,當我退出Widget P時,界面就變成了Widget O,這是沒問題的,然后我繼續退出WIdget O,如果不進行特殊處理的話,FlutterEngine將會展示Widget N,這顯然不是我們想要的,我們預期的結果是此時要finish掉HostActivity 實體2,展示NativeOneActivity,想要達到這樣的效果,就需要認為的把FlutterEngine 堆疊里面在不同HostActivity實體中顯示的Widget進行特殊的區分,
好了,理論作業已經做完了,接下來就是愉快的Coding時光,我們面臨任務清單有以下幾個:
一、如何對FlutterEngine進行復用?
二、FlutterActivity、FlutterFragment還是FlutterView?
三、如何監聽FlutterEngine的堆疊變化資訊?
四、如何把監聽到的堆疊資訊同步到宿主Activity實體?
五、如何處理宿主Activity和Flutter頁面的同步?
第一個問題,首先實體化FlutterEngine:
/**
* 初始化FlutterEngine
* @param context 背景關系
* @param block 初始化狀態回呼 0 不需要初始化 1 開始初始化 2 初始化完畢
*/
@Synchronized
fun initFlutterEngine(context: Context, block: (status: Int) -> Unit) {
//判斷快取已存在
if (!FlutterEngineCache.getInstance().contains(FLUTTER_ENGINE)) {
block(1)
//初始化FlutterEngine
var engine = FlutterEngine(context.applicationContext)
//初始化BasicMessageChannel
messageChannel = BasicMessageChannel(
engine.dartExecutor,
FLUTTER_MIX_STACK_CHANNEL,
StringCodec.INSTANCE
)
//添加訊息監聽
messageChannel?.setMessageHandler(messageHandler)
this.initCallback = block
//開始運行dart代碼
engine.dartExecutor.executeDartEntrypoint(
DartExecutor.DartEntrypoint.createDefault()
)
//快取FlutterEngine
FlutterEngineCache.getInstance().put(FLUTTER_ENGINE, engine)
} else {
block(0)
}
}
FlutterEngineCache已經幫我們封裝好了FlutterEngine的快取功能,不過要怎么使用這個快取呢?
官方提供的FlutterActivity類中有一個withCachedEngine的靜態方法,可以幫助我們獲取使用快取的Intent實體,也可以繼承FlutterActivity重寫getCachedEngineId方法,達到FlutterEngine復用的效果,直接啟動FlutterActivity就可以加載Flutter頁面,非常的簡單方便,但是如果有一些稍微復雜的場景時,FlutterActivity就有些不太夠用了,
第二個問題,Flutter 2.0 為我們提供了三種Flutter容器,分別是FlutterActivity(FlutterFragmentActivity)、FlutterFragment和FlutterView,以滿足我們不同場景下的使用,從這三個類注釋檔案的豐富程度上來看,官方是非常推薦我們直接使用FlutterActivity的,簡單看下原始碼就能發現,不管是FlutterActivity還是FLutterFragment,都是基于FlutterView實作的,如果專案有指定的基類需要繼承或者要實作原生UI+Flutter UI的情況,或是想要更早的預熱Flutter的Widget,FlutterView無疑會更靈活一些,所以這里我就選擇使用了普通的Activity+FlutterView,偽代碼如下:
class HostActivity : AppCompatActivity(){
var flutterEngine //快取中拿到的FlutterEngine
var flutterChannel // BasicMessageChannel 下面會有專門的介紹
var flutterView
onCreate(){
//在onCreate方法呼叫之初,就提前預熱FlutterEngine,可以使Widget加載更流暢
flutterEngine.lifecycleChannel.appIsResumed()
//同樣在onCreate中發送訊息給FluuterEngine,提前替換我們要加載的Widget,方式有過渡效果
flutterChannel.sendMessage(routePath)
super.onCreate()
setContentView(flutterView = createFlutterView())
}
onResume(){
super()
flutterEngine.lifecycleChannel.appIsResumed()
}
onPause(){
super()
flutterEngine.lifecycleChannel.appIsPaused()
}
onDestroy(){
super()
flutterView.detachFromFlutterEngine()
}
}
第三個問題,監聽FlutterEngine里Widget堆疊資訊的變化,在MaterialApp初始化的時候,有一個navigatorObservers的引數,支持我們添加navigator的變化資訊(Navigator是Flutter提供的頁面切換類),
void main() {
//這里使用了Get框架來演示,但這不是必須的
var getApp = GetMaterialApp(
initialRoute: RouterMapper.ROUTER_HOME,
getPages: [
GetPage(
name: RouterMapper.ROUTER_HOST, //空白的HostView
page: () => Host(),
transition: Transition.rightToLeft),
...
],
navigatorObservers: [RouteNavigatorObserver()],
);
runApp(getApp);
RouterHelper.registerApp(); //在這里注冊訊息監聽
}
class RouteNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 有Widget入堆疊 可以從route中獲取name資訊并同步到Native
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 有Widget出堆疊 可以從route中獲取name資訊并同步到Native
}
@override
void didReplace({Route<dynamic>? newRoute, Route<dynamic>? oldRoute}) {
// 新的Widget替換了前一個Widget 可以從newRoute&oldRoute中獲取name資訊并同步到Native
}
@override
void didRemove(Route<dynamic> route, Route<dynamic>? previousRoute) {
// 一個Widget被remove了,一般不建議這么做
}
}
通過繼承NavigatorObserver類并注冊到navigatorObservers中,我們實作了對FlutterEngine中路由資訊變化的監聽,那么接著來下一個問題,我們如何把訊息同步給Native?
**第四個問題,**Flutter提供了三種方式用來和Native互動資料,分別是BasicMessageChannel,MethodChannel和EventChannel,其中前兩個是可以雙向傳遞資料的,EventChannel只支持Native給Flutter傳遞資料,常用語系統訊息的一個通知,MethodChannel用來傳遞方法呼叫,BasicMessageChannel用來傳二進制資料,更適合我們的試用場景,接下來我們需要在Flutter和Native層住的BasicMessageChannel用來資料的互動,
那我們分別在Flutter和Native層創建BasicMessageChannel:
//Flutter
class RouterHelper {
//注冊訊息通道
static final _routerMessageChannel =
BasicMessageChannel<String?>("flutter_router_channel", StringCodec());
static registerApp() {
//防止下面空例外
WidgetsFlutterBinding.ensureInitialized();
//注冊訊息監聽
_routerMessageChannel.setMessageHandler((String? message) async {
if (message != null) {
//native層傳遞過來的訊息
}
});
}
}
//Kotlin
fun initMessageChannel(){
var messageChannel = BasicMessageChannel(
engine.dartExecutor,
"flutter_router_channel",
StringCodec.INSTANCE)
messageChannel.setMessageHandler{ message, reply ->
//處理Flutter發送的訊息
}
}
訊息的發送由BasicMessageChannel提供的send方法實作,在上面的一個問題中,我們已經通過監聽Navigator來判斷Flutter中頁面的變化資訊,接下來就要在監聽方法中,把對應的資訊發送到Native層進行處理,在Native層我們可以抽象出來一個介面,讓HostActivity實作這介面,通過面向介面編程的方式,把訊息分發到當前活躍的HostActivity中,而HostActivity中維護了一個堆疊來記錄Flutter中堆疊資訊的變化,
/**
* flutter路由變化回呼
*/
interface RouteCallback {
//flutter route入堆疊訊息
fun onPush(route: String)
//flutter route出堆疊訊息
fun onPop(route: String)
//flutter route堆疊替換訊息
fun onReplace(newRoute: String, oldRoute: String)
//flutter route被移除的訊息
fun onRemove(route: String)
//flutter申請打開Native頁面的訊息
fun routeNative(nativeRoute: String, params: HashMap<String, Any>? = null)
//flutter route出堆疊訊息
fun getLifecycleOwner(): LifecycleOwner
}
class HostActivity : AppCompatActivity(), RouteCallback {
//記錄flutter的路由堆疊資訊
private val routeStack: Stack<String> by lazy {
Stack()
}
//TODO RouteCallback介面的實作方法,在方法中對routeStack進行入堆疊和出堆疊操作
}
最后一個問題,我們需要用戶進行回傳操作的時候,Widget頁面要和HostActivity保持同步,例如當前HostActivity只打開了一個Widget,那么這個Widget退出的時候,HostActivity也要同步finish掉,即使FlutterEngine中還存在其他的Widget,我們可以通過HostActivity中維護的routeStack來實作,
override fun onBackPressed() {
//判斷當前堆疊的長度,如果當前HostActivity堆疊的長度小于等于1,那么Activity就要finsih
if (routeStack.size <= 1) {
super.onBackPressed()
flutterEngine.navigationChannel.popRoute()
}
//正常執行Navigator的pop操作
else {
flutterEngine.navigationChannel.popRoute()
}
}
當然還有一個問題需要注意,有些Widget的回傳并不是通過回傳鍵處理的,那我們就需對onPop方法進行一些特殊處理:
override fun onPop(route: String) {
//正常處理出堆疊邏輯
if (routeStack.isEmpty() && !isFinishing) {
//如果已經全部出堆疊,則當前HostActivity的業務已完成
finish()
}
}
這樣,就能把HostActivity和該宿主內打開的Widget進行關聯,做到共同進退,主要的邏輯實作起來也不負責,也不需要使用任何第三方的框架,這樣就有了很大的靈活性,尤其是對Flutter來說,Flutter的生態還不是十分成熟,避免使用侵入性過大的框架,也為以后技術的迭代留有足夠的空間,
缺點和不足
雖然通過對Flutter內堆疊資訊變化的監聽和通過BasicMessageChannel的訊息同步來做好混合堆疊管理,但是目前依然有一些問題需要解決,
1:Flutter和Native頁面的回傳值監聽,OneActivity打開了一個HostActivity并想要在HostActivity退出的時候給OneActivity一個回傳值,這個功能實體代碼中還沒有實作,等有空繼續完善該框架,
2:跨域回傳的問題,這個“跨域”是我自己定義的一個名字,就是如果我打開了兩個HostActivity,第二個HostActivity通過Navigator的remove方法想要關閉掉第一個HostActivity中打開的Widget,這個是可以做到了,因為這兩個HostActivity使用的是同一個FlutterEngine,如果出現這種操作,那就會讓第一個HostActivity的頁面顯示出現問題,其實我在想,理想的混合管理框架應該是把每個打開的HostActivity當做一個WebView來看待,想要在第二個WebView中關閉第一個WebView里打開的頁面,這顯然也是不太合適的,這種跨域回傳是否需要支持,也是一個值得考慮的問題,
最后
小編在網上收集了一些 Android 開發相關的學習檔案、面試題、Android 核心筆記等等檔案,希望能幫助到大家學習提升,如有需要參考的可以直接去我 CodeChina地址:https://codechina.csdn.net/u012165769/Android-T3 訪問查閱,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/287762.html
標籤:其他
上一篇:來深入了解一波鴻蒙開發
