為什麼寫thrio?
在早期Flutter釋出的時候,谷歌雖然提供了iOS和Android App上的Flutter嵌入方案,但主要針對的是純Flutter的情形,混合開發支援的並不友好。
所謂的純RN、純weex應用的生命週期都不存在,所以也不會存在一個純Flutter的App的生命週期,因為我們總是有需要複用現有模組。
所以我們需要一套足夠完整的Flutter嵌入原生App的路由解決方案,所以我們自己造了個輪子 thrio,現已開源,遵循MIT協議。
thrio的設計原則原則一,dart端最小改動接入原則二,原生端最小侵入原則三,三端保持一致的APIthrio所有功能的設計,都會遵守這三個原則。下面會逐步對功能層面一步步展開進行說明,後面也會有原理性的解析。thrio的頁面路由以dart中的 Navigator 為主要參照,提供以下路由能力:
push,開啟一個頁面並放到路由棧頂pop,關閉路由棧頂的頁面popTo,關閉到某一個頁面remove,刪除任意頁面Navigator中的API幾乎都可以通過組合以上方法實現,replace 方法暫未提供。
不提供iOS中存在的 present 功能,因為會導致原生路由棧被覆蓋,維護複雜度會非常高,如確實需要可以通過修改轉場動畫實現。
頁面的索引
要路由,我們需要對頁面建立索引,通常情況下,我們只需要給每個頁面設定一個 url 就可以了,如果每個頁面都只打開一次的話,不會有任何問題。但是當一個頁面被開啟多次之後,僅僅通過url是無法定位到明確的頁面例項的,所以在 thrio 中我們增加了頁面索引的概念,具體在API中都會以 index 來表示,同一個url第一個開啟的頁面的索引為 1 ,之後同一個 url 的索引不斷累加。
如此,唯一定位一個頁面的方式為 url + index,在dart中 route 的 name 就是由 '$url.$index' 組合而成。
大多數場景下,使用者不需要關注 index,只有當需要定位到多開的 url 的頁面中的某一個時才需要關注 index。最簡單獲取 index 的方式為 push 方法的回撥返回值。
頁面的push
dart 端開啟頁面
ThrioNavigator.push(url: 'flutter1');// 傳入引數ThrioNavigator.push(url: 'native1', params: { '1': {'2': '3'}});// 是否動畫,目前在內嵌的dart頁面中動畫無法取消,原生iOS頁面有效果ThrioNavigator.push(url: 'native1', animated:true);// 接收鎖開啟頁面的關閉回撥ThrioNavigator.push( url: 'biz2/flutter2', params: {'1': {'2': '3'}}, poppedResult: (params) => ThrioLogger.v('biz2/flutter2 popped: $params'),);
iOS 端開啟頁面
[ThrioNavigator pushUrl:@"flutter1"];// 接收所開啟頁面的關閉回撥[ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) { ThrioLogV(@"biz2/flutter2 popped: %@", params);}];
Android 端開啟頁面
ThrioNavigator.push(this, "biz1/flutter1", mapOf("k1" to 1), false, poppedResult = { Log.e("Thrio", "native1 popResult call params $it") })
連續開啟頁面
dart端只需要await push,就可以連續開啟頁面原生端需要等待push的result回撥返回才能開啟第二個頁面獲取所開啟頁面關閉後的回撥引數
三端都可以通過閉包 poppedResult 來獲取頁面的pop
dart 端關閉頂層頁面
// 預設動畫開啟ThrioNavigator.pop();// 不開啟動畫,原生和dart頁面都生效ThrioNavigator.pop(animated: false);// 關閉當前頁面,並傳遞引數給push這個頁面的回撥ThrioNavigator.pop(params: 'popped flutter1');
iOS 端關閉頂層頁面
// 預設動畫開啟[ThrioNavigator pop];// 關閉動畫[ThrioNavigator popAnimated:NO];// 關閉當前頁面,並傳遞引數給push這個頁面的回撥[ThrioNavigator popParams:@{@"k1": @3}];
Android 端關閉頂層頁面
ThrioNavigator.pop(this, params, animated);
頁面的popTo
dart 端關閉到頁面
// 預設動畫開啟ThrioNavigator.popTo(url: 'flutter1');// 不開啟動畫,原生和dart頁面都生效ThrioNavigator.popTo(url: 'flutter1', animated: false);
iOS 端關閉到頁面
// 預設動畫開啟[ThrioNavigator popToUrl:@"flutter1"];// 關閉動畫[ThrioNavigator popToUrl:@"flutter1" animated:NO];
Android 端關閉到頁面
ThrioNavigator.popTo(context, url, index);
頁面的remove
dart 端關閉特定頁面
ThrioNavigator.remove(url: 'flutter1');// 只有當頁面是頂層頁面時,animated引數才會生效ThrioNavigator.remove(url: 'flutter1', animated: true);
iOS 端關閉特定頁面
[ThrioNavigator removeUrl:@"flutter1"];// 只有當頁面是頂層頁面時,animated引數才會生效[ThrioNavigator removeUrl:@"flutter1" animated:NO];
Android 端關閉特定頁面
ThrioNavigator.remove(context, url, index);
thrio的頁面通知
頁面通知一般來說並不在路由的範疇之內,但我們在實際開發中卻經常需要使用到,由此產生的各種模組化框架一個比一個複雜。那麼問題來了,這些模組化框架很難在三端互通,所有的這些模組化框架提供的能力無非最終是一個頁面通知的能力,而且頁面通知我們可以非常簡單的在三端打通。鑑於此,頁面通知作為thrio的一個必備能力被引入了thrio。
dart 端給特定頁面發通知
ThrioNavigator.notify(url: 'flutter1', name: 'reload');
dart 端接收頁面通知
使用 NavigatorPageNotify 這個 Widget 來實現在任何地方接收當前頁面收到的通知。
NavigatorPageNotify( name: 'page1Notify', onPageNotify: (params) => ThrioLogger.v('flutter1 receive notify: $params'), child: Xxxx());
iOS 端給特定頁面發通知
[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
iOS 端接收頁面通知
UIViewController實現協議NavigatorPageNotifyProtocol,通過 onNotify 來接收頁面通知
-(void)onNotify:(NSString )name params:(NSDictionary )params { ThrioLogV(@”native1 onNotify: %@, %@”, name, params);}
Android 端給特定頁面發通知
ThrioNavigator.notify(url, index, params);
Android 端接收頁面通知
Activity實現協議OnNotifyListener,通過 onNotify 來接收頁面通知
class Activity : AppCompatActivity(), OnNotifyListener { override fun onNotify(name: String, params: Any?) { }}
因為Android activity在後臺可能會被銷燬,所以頁面通知實現了一個懶響應的行為,只有當頁面呈現之後才會收到該通知,這也符合頁面需要重新整理的場景。
thrio的模組化模組化在thrio裡面只是一個非核心功能,僅僅為了實現原則二而引入原生端。
thrio的模組化能力由一個類提供,ThrioModule,很小巧,主要提供了 Module 的註冊鏈和初始化鏈,讓程式碼可以根據路由url進行檔案分級分類。
註冊鏈將所有模組串起來,字母塊由最近的父一級模組註冊,新增模組的耦合度最低。
初始化鏈將所有模組需要初始化的程式碼串起來,同樣是為了降低耦合度,在初始化鏈上可以就近註冊模組的頁面的構造器,頁面路由觀察者,頁面生命週期觀察者等,也可以在多引擎模式下提前啟動某一個引擎。
模組間通訊的能力由頁面通知實現。
mixin ThrioModule { /// A function for registering a module, which will call /// the `onModuleRegister` function of the `module`. /// void registerModule(ThrioModule module); /// A function for module initialization that will call /// the `onPageRegister`, `onModuleInit` and `onModuleAsyncInit` /// methods of all modules. /// void initModule(); /// A function for registering submodules. /// void onModuleRegister() {} /// A function for registering a page builder. /// void onPageRegister() {} /// A function for module initialization. /// void onModuleInit() {} /// A function for module asynchronous initialization. /// void onModuleAsyncInit() {} /// Register an page builder for the router. /// /// Unregistry by calling the return value `VoidCallback`. /// VoidCallback registerPageBuilder(String url, NavigatorPageBuilder builder); /// Register observers for the life cycle of Dart pages. /// /// Unregistry by calling the return value `VoidCallback`. /// /// Do not override this method. /// VoidCallback registerPageObserver(NavigatorPageObserver pageObserver); /// Register observers for route action of Dart pages. /// /// Unregistry by calling the return value `VoidCallback`. /// /// Do not override this method. /// VoidCallback registerRouteObserver(NavigatorRouteObserver routeObserver);}
thrio的頁面生命週期原生端可以獲得所有頁面的生命週期,Dart 端只能獲取自身頁面的生命週期。
dart 端獲取頁面的生命週期
class Module with ThrioModule, NavigatorPageObserver {\t@override\tvoid onPageRegister() { \t\tregisterPageObserver(this);\t} \t@override\tvoid didAppear(RouteSettings routeSettings) {} \t@override\tvoid didDisappear(RouteSettings routeSettings) {} \t@override\tvoid onCreate(RouteSettings routeSettings) {} \t@override\tvoid willAppear(RouteSettings routeSettings) {} \t@override\tvoid willDisappear(RouteSettings routeSettings) {}}
iOS 端獲取頁面的生命週期
@interface Module1 : ThrioModule@end@implementation Module1-(void)onPageRegister {[self registerPageObserver:self];}-(void)onCreate:(NavigatorRouteSettings *)routeSettings { }-(void)willAppear:(NavigatorRouteSettings *)routeSettings { }-(void)didAppear:(NavigatorRouteSettings *)routeSettings { }-(void)willDisappear:(NavigatorRouteSettings *)routeSettings { }-(void)didDisappear:(NavigatorRouteSettings *)routeSettings { }@end
thrio的頁面路由觀察者原生端可以觀察所有頁面的路由行為,dart 端只能觀察 dart 頁面的路由行為
dart 端獲取頁面的路由行為
class Module with ThrioModule, NavigatorRouteObserver {\t@override\tvoid onModuleRegister() { \t\tregisterRouteObserver(this);\t} \t@override\tvoid didPop( \t\tRouteSettings routeSettings, \t\tRouteSettings previousRouteSettings,\t) {} \t@override\tvoid didPopTo( \t\tRouteSettings routeSettings, \t\tRouteSettings previousRouteSettings,\t) {} \t@override\tvoid didPush( \t\tRouteSettings routeSettings, \t\tRouteSettings previousRouteSettings,\t) {} \t@override\tvoid didRemove( \t\tRouteSettings routeSettings, \t\tRouteSettings previousRouteSettings,\t) {}}
iOS 端獲取頁面的路由行為
@interface Module2 : ThrioModule@end@implementation Module2-(void)onPageRegister { [self registerRouteObserver:self];}-(void)didPop:(NavigatorRouteSettings )routeSettings previousRoute:(NavigatorRouteSettings _Nullable)previousRouteSettings {}-(void)didPopTo:(NavigatorRouteSettings )routeSettings previousRoute:(NavigatorRouteSettings _Nullable)previousRouteSettings {}-(void)didPush:(NavigatorRouteSettings )routeSettings previousRoute:(NavigatorRouteSettings _Nullable)previousRouteSettings {}-(void)didRemove:(NavigatorRouteSettings )routeSettings previousRoute:(NavigatorRouteSettings _Nullable)previousRouteSettings {}@end
thrio的額外功能iOS 顯示或隱藏當前頁面的導航欄
原生的導航欄在 dart 上一般情況下是不需要的,但切換到原生頁面又需要把原生的導航欄置回來,thrio 不提供的話,使用者較難擴充套件,我之前在目前一個主流的Flutter接入庫上進行此項功能的擴充套件,很不流暢,所以這個功能最好的效果還是 thrio 直接內建,切換到 dart 頁面預設會隱藏原生的導航欄,切回原生頁面也會自動恢復。另外也可以手動隱藏原生頁面的導航欄。
viewController.thrio_hidesNavigationBar = NO;
支援頁面關閉前彈窗確認的功能
如果使用者正在填寫一個表單,你可能經常會需要彈窗確認是否關閉當前頁面的功能。在 dart 中,有一個 Widget 提供了該功能,thrio 完好的保留了這個功能。
WillPopScope( onWillPop: () async => true, child: Container(),);
在 iOS 中,thrio 提供了類似的功能,返回 NO 表示不會關閉,一旦設定會將側滑返回手勢禁用
viewController.thrio_willPopBlock = ^(ThrioBoolCallback _Nonnull result) {\tresult(NO);};
關於 FlutterViewController 的側滑返回手勢,Flutter 預設支援的是純Flutter應用,僅支援單一的 FlutterViewController 作為整個App的容器。
但 thrio 要解決的是 Flutter 與原生應用的無縫整合,所以必須將側滑返回的手勢加回來。
thrio的設計解析目前開源 Flutter 嵌入原生的庫,主要的還是通過切換 FlutterEngine 上的原生容器來實現的,這是 Flutter 原本提供的原生容器之上最小改動而實現,需要小心處理好容器切換的時序,否則在頁面導航時會產生崩潰。基於 Flutter 提供的這個功能, thrio 構建了三端一致的頁面管理API。
dart 的核心類
dart 端只管理 dart頁面
基於 RouteSettings 進行擴充套件,複用現有的欄位
name = url.indexisInitialRoute = !isNestedarguments = params基於 MaterialPageRoute 擴充套件的 NavigatorPageRoute
主要提供頁面描述和轉場動畫的是否配置的功能基於 Navigator 擴充套件,封裝 NavigatorWidget,提供以下方法
Future<bool> push(RouteSettings settings, { bool animated = true, NavigatorParamsCallback poppedResult, }); Future<bool> pop(RouteSettings settings, {bool animated = true}); Future<bool> popTo(RouteSettings settings, {bool animated = true}); Future<bool> remove(RouteSettings settings, {bool animated = false});
封裝 `ThrioNavigator` 路由API
abstract class ThrioNavigator { /// Push the page onto the navigation stack. /// /// If a native page builder exists for the `url`, open the native page, /// otherwise open the flutter page. /// static Future<int> push({ @required String url, params, bool animated = true, NavigatorParamsCallback poppedResult, }); /// Send a notification to the page. /// /// Notifications will be triggered when the page enters the foreground. /// Notifications with the same `name` will be overwritten. /// static Future<bool> notify({ @required String url, int index, @required String name, params, }); /// Pop a page from the navigation stack. /// static Future<bool> pop({params, bool animated = true}) static Future<bool> popTo({ @required String url, int index, bool animated = true, }); /// Remove the page with `url` in the navigation stack. /// static Future<bool> remove({ @required String url, int index, bool animated = true, });}
iOS 的核心類
NavigatorRouteSettings 對應於 dart 的 RouteSettings 類,並提供相同資料結構
@interface NavigatorRouteSettings : NSObject@property (nonatomic, copy, readonly) NSString *url;@property (nonatomic, strong, readonly) NSNumber *index;@property (nonatomic, assign, readonly) BOOL nested;@property (nonatomic, copy, readonly, nullable) id params;@end
NavigatorPageRoute 對應於 dart 的 NavigatorPageRoute 類
儲存通知、頁面關閉回撥、NavigatorRouteSettingsroute的雙向連結串列基於 UINavigationController 擴充套件,功能類似 dart 的 NavigatorWidget
提供一些列的路由內部介面並能相容非 thrio 體系內的頁面基於 UIViewController 擴充套件
提供 FlutterViewController 容器上的 dart 頁面的管理功能提供 popDisable 等功能封裝 ThrioNavigator 路由API
@interface ThrioNavigator : NSObject/// Push the page onto the navigation stack.////// If a native page builder exists for the url, open the native page,/// otherwise open the flutter page.///+ (void)pushUrl:(NSString *)url params:(id)params animated:(BOOL)animated result:(ThrioNumberCallback)result poppedResult:(ThrioIdCallback)poppedResult;/// Send a notification to the page.////// Notifications will be triggered when the page enters the foreground./// Notifications with the same name will be overwritten.///+ (void)notifyUrl:(NSString *)url index:(NSNumber *)index name:(NSString *)name params:(id)params result:(ThrioBoolCallback)result;/// Pop a page from the navigation stack.///+ (void)popParams:(id)params animated:(BOOL)animated result:(ThrioBoolCallback)result;/// Pop the page in the navigation stack until the page with `url`.///+ (void)popToUrl:(NSString *)url index:(NSNumber *)index animated:(BOOL)animated result:(ThrioBoolCallback)result;/// Remove the page with `url` in the navigation stack.///+ (void)removeUrl:(NSString *)url index:(NSNumber *)index animated:(BOOL)animated result:(ThrioBoolCallback)result;@end
dart 與 iOS 路由棧的結構
一個應用允許啟動多個Flutter引擎,可讓每個引擎執行的程式碼物理隔離,按需啟用,劣勢是啟動多個Flutter引擎可能導致資源消耗過多而引起問題;一個Flutter引擎通過切換可以匹配到多個FlutterViewController,這是Flutter優雅嵌入原生應用的前提條件一個FlutterViewController可以內嵌多個Dart頁面,有效減少單個FlutterViewController只打開一個Dart頁面導致的記憶體消耗過多問題,關於記憶體消耗的問題,後續會有提到。dart 與 iOS push的時序圖
所有路由操作最終匯聚於原生端開始,如果始於 dart 端,則通過 channel 呼叫原生端的API通過 url+index 定位到頁面如果頁面是原生頁面,則直接進行相關操作如果頁面是 Flutter 容器,則通過 channel 呼叫 dart 端對應的路由 API接4步,如果 dart 端對應的路由 API 操作完成後回撥,如果成功,則執行原生端的路由棧同步,如果失敗,則回撥入口 API 的result接4不,如果 dart 端對應的路由 API操作成功,則通過 route channel 呼叫原生端對應的 route observer,通過 page channel 呼叫原生端對應的 page observer。dart 與 iOS pop的時序圖
pop 的流程與 push 基本一致;pop 需要考慮頁面是否可關閉的問題;但在 iOS 中,側滑返回手勢會導致問題, popViewControllerAnimated: 會在手勢開始的時候呼叫,導致 dart 端的頁面已經被 pop 掉,但如果手勢被放棄了,則導致兩端的頁面棧不一致,thrio 已經解決了這個問題,具體流程稍複雜,原始碼可能更好的說明。dart 與 iOS popTo的時序圖
popTo 的流程與 push 基本一致;但在多引擎模式下,popTo需要處理多引擎的路由棧同步的問題;另外在 Dart 端,popTo實際上是多個pop或者remove構成的,最終產生多次的didPop或didRemove行為,需要將多個pop或remove組合起來形成一個didPopTo行為。dart 與 iOS remove的時序圖
remove 的流程與 push 基本一致。總結目前 Flutter 接入原生應用主流的解決方案應該是 boost,筆者的團隊在專案深度使用過 boost,也積累了很多對 boost 改善的需求,遇到的最大問題是記憶體問題,每開啟一個 Flutter 頁面的記憶體開銷基本到了很難接受的程度,thrio 把解決記憶體問題作為頭等任務,最終效果還是不錯的,比如以連續開啟 5 個 Flutter 頁面計算,boost 的方案會消耗 91.67M 記憶體,thrio 只消耗 42.76 記憶體,模擬器上跑出來的資料大致如下:
同樣連續開啟 5 個頁面的場景,thrio 開啟第一個頁面跟 boost 耗時是一樣的,因為都需要開啟一個新的 Activity,之後 4 個頁面 thrio 會直接開啟 Flutter 頁面,耗時會降下來,以下單位為 ms:
當然,thrio 跟 boost 的定位還是不太一樣的,thrio 更多的偏向於解決我們業務上的需求,儘量做到開箱即用。