在早期Flutter發佈的時候,谷歌雖然提供了iOS和Android App上的Flutter嵌入方案,但主要針對的是純Flutter的情形,混合開發支持的並不友好。git
所謂的純RN、純weex應用的生命週期都不存在,因此也不會存在一個純Flutter的App的生命週期,由於咱們老是有須要複用現有模塊。github
因此咱們須要一套足夠完整的Flutter嵌入原生App的路由解決方案,因此咱們本身造了個輪子 thrio ,現已開源,遵循MIT協議。markdown
thrio全部功能的設計,都會遵照這三個原則。下面會逐步對功能層面一步步展開進行說明,後面也會有原理性的解析。weex
以dart中的 Navigator
爲主要參照,提供如下路由能力:數據結構
Navigator中的API幾乎均可以經過組合以上方法實現,replace
方法暫未提供。閉包
不提供iOS中存在的 present
功能,由於會致使原生路由棧被覆蓋,維護複雜度會很是高,如確實須要能夠經過修改轉場動畫實現。app
要路由,咱們須要對頁面創建索引,一般狀況下,咱們只須要給每一個頁面設定一個 url
就能夠了,若是每一個頁面都只打開一次的話,不會有任何問題。可是當一個頁面被打開屢次以後,僅僅經過url是沒法定位到明確的頁面實例的,因此在 thrio
中咱們增長了頁面索引的概念,具體在API中都會以 index
來表示,同一個url第一個打開的頁面的索引爲 1
,以後同一個 url
的索引不斷累加。框架
如此,惟必定位一個頁面的方式爲 url
+ index
,在dart中 route
的 name
就是由 '$url.$index'
組合而成。async
不少時候,使用者不須要關注 index
,只有當須要定位到多開的 url
的頁面中的某一個時才須要關注 index
。最簡單獲取 index
的方式爲 push
方法的回調返回值。ide
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'),
);
複製代碼
[ThrioNavigator pushUrl:@"flutter1"];
// 接收所打開頁面的關閉回調
[ThrioNavigator pushUrl:@"biz2/flutter2" poppedResult:^(id _Nonnull params) {
ThrioLogV(@"biz2/flutter2 popped: %@", params);
}];
複製代碼
ThrioNavigator.push(this, "biz1/flutter1",
mapOf("k1" to 1),
false,
poppedResult = {
Log.e("Thrio", "native1 popResult call params $it")
}
)
複製代碼
// 默認動畫開啓
ThrioNavigator.pop();
// 不開啓動畫,原生和dart頁面都生效
ThrioNavigator.pop(animated: false);
// 關閉當前頁面,並傳遞參數給push這個頁面的回調
ThrioNavigator.pop(params: 'popped flutter1'),
複製代碼
// 默認動畫開啓
[ThrioNavigator pop];
// 關閉動畫
[ThrioNavigator popAnimated:NO];
// 關閉當前頁面,並傳遞參數給push這個頁面的回調
[ThrioNavigator popParams:@{@"k1": @3}];
複製代碼
ThrioNavigator.pop(this, params, animated)
複製代碼
// 默認動畫開啓
ThrioNavigator.popTo(url: 'flutter1');
// 不開啓動畫,原生和dart頁面都生效
ThrioNavigator.popTo(url: 'flutter1', animated: false);
複製代碼
// 默認動畫開啓
[ThrioNavigator popToUrl:@"flutter1"];
// 關閉動畫
[ThrioNavigator popToUrl:@"flutter1" animated:NO];
複製代碼
ThrioNavigator.popTo(context, url, index)
複製代碼
ThrioNavigator.remove(url: 'flutter1');
// 只有當頁面是頂層頁面時,animated參數纔會生效
ThrioNavigator.remove(url: 'flutter1', animated: true);
複製代碼
[ThrioNavigator removeUrl:@"flutter1"];
// 只有當頁面是頂層頁面時,animated參數纔會生效
[ThrioNavigator removeUrl:@"flutter1" animated:NO];
複製代碼
ThrioNavigator.remove(context, url, index)
複製代碼
頁面通知通常來講並不在路由的範疇以內,但咱們在實際開發中卻常常須要使用到,由此產生的各類模塊化框架一個比一個複雜。
那麼問題來了,這些模塊化框架很難在三端互通,全部的這些模塊化框架提供的能力無非最終是一個頁面通知的能力,並且頁面通知咱們能夠很是簡單的在三端打通。
鑑於此,頁面通知做爲thrio的一個必備能力被引入了thrio。
ThrioNavigator.notify(url: 'flutter1', name: 'reload');
複製代碼
[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
複製代碼
ThrioNavigator.notify(url, index, params)
複製代碼
使用 NavigatorPageNotify
這個 Widget
來實如今任何地方接收當前頁面收到的通知。
NavigatorPageNotify(
name: 'page1Notify',
onPageNotify: (params) =>
ThrioLogger.v('flutter1 receive notify: $params'),
child: Xxxx());
複製代碼
UIViewController
實現協議NavigatorPageNotifyProtocol
,經過 onNotify
來接收頁面通知
- (void)onNotify:(NSString *)name params:(NSDictionary *)params {
ThrioLogV(@"native1 onNotify: %@, %@", name, params);
}
複製代碼
Activity
實現協議OnNotifyListener
,經過 onNotify
來接收頁面通知
class Activity : AppCompatActivity(), OnNotifyListener {
override fun onNotify(name: String, params: Any?) {
}
}
複製代碼
由於Android activity在後臺可能會被銷燬,因此頁面通知實現了一個懶響應的行爲,只有當頁面呈現以後纔會收到該通知,這也符合頁面須要刷新的場景。
模塊化在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);
}
複製代碼
原生端能夠得到全部頁面的生命週期,Dart 端只能獲取自身頁面的生命週期
class Module with ThrioModule, NavigatorPageObserver {
@override
void onPageRegister() {
registerPageObserver(this);
}
@override
void didAppear(RouteSettings routeSettings) {}
@override
void didDisappear(RouteSettings routeSettings) {}
@override
void onCreate(RouteSettings routeSettings) {}
@override
void willAppear(RouteSettings routeSettings) {}
@override
void willDisappear(RouteSettings routeSettings) {}
}
複製代碼
@interface Module1 : ThrioModule<NavigatorPageObserverProtocol>
@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
複製代碼
原生端能夠觀察全部頁面的路由行爲,dart 端只能觀察 dart 頁面的路由行爲
class Module with ThrioModule, NavigatorRouteObserver {
@override
void onModuleRegister() {
registerRouteObserver(this);
}
@override
void didPop(
RouteSettings routeSettings,
RouteSettings previousRouteSettings,
) {}
@override
void didPopTo(
RouteSettings routeSettings,
RouteSettings previousRouteSettings,
) {}
@override
void didPush(
RouteSettings routeSettings,
RouteSettings previousRouteSettings,
) {}
@override
void didRemove(
RouteSettings routeSettings,
RouteSettings previousRouteSettings,
) {}
}
複製代碼
@interface Module2 : ThrioModule<NavigatorRouteObserverProtocol>
@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
複製代碼
原生的導航欄在 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) {
result(NO);
};
複製代碼
關於 FlutterViewController
的側滑返回手勢,Flutter 默認支持的是純Flutter應用,僅支持單一的 FlutterViewController
做爲整個App的容器,內部已經將 FlutterViewController
的側滑返回手勢去掉。但 thrio 要解決的是 Flutter 與原生應用的無縫集成,因此必須將側滑返回的手勢加回來。
目前開源 Flutter 嵌入原生的庫,主要的仍是經過切換 FlutterEngine 上的原生容器來實現的,這是 Flutter 本來提供的原生容器之上最小改動而實現,須要當心處理好容器切換的時序,不然在頁面導航時會產生崩潰。基於 Flutter 提供的這個功能, thrio 構建了三端一致的頁面管理API。
dart 端只管理 dart頁面
RouteSettings
進行擴展,複用現有的字段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
路由APIabstract 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,
});
}
複製代碼
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
類UINavigationController
擴展,功能相似 dart 的 NavigatorWidget
UIViewController
擴展FlutterViewController
容器上的 dart 頁面的管理功能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
複製代碼
url+index
定位到頁面popViewControllerAnimated:
會在手勢開始的時候調用,致使 dart 端的頁面已經被 pop 掉,但若是手勢被放棄了,則致使兩端的頁面棧不一致,thrio 已經解決了這個問題,具體流程稍複雜,源碼可能更好的說明。目前 Flutter 接入原生應用主流的解決方案應該是boost,筆者的團隊在項目深度使用過 boost,也積累了不少對 boost 改善的需求,遇到的最大問題是內存問題,每打開一個 Flutter 頁面的內存開銷基本到了很難接受的程度,thrio把解決內存問題做爲頭等任務,最終效果仍是不錯的,好比以連續打開 5 個 Flutter 頁面計算,boost 的方案會消耗 91.67M 內存,thrio 只消耗 42.76 內存,模擬器上跑出來的數據大體以下:
demo | 啓動 | 頁面 1 | 頁面 2 | 頁面 3 | 頁面 4 | 頁面 5 |
---|---|---|---|---|---|---|
thrio | 8.56 | 37.42 | 38.88 | 42.52 | 42.61 | 42.76 |
boost | 6.81 | 36.08 | 50.96 | 66.18 | 78.86 | 91.67 |
一樣連續打開 5 個頁面的場景,thrio 打開第一個頁面跟 boost 耗時是同樣的,由於都須要打開一個新的 Activity,以後 4 個頁面 thrio 會直接打開 Flutter 頁面,耗時會降下來,如下單位爲 ms:
demo | 頁面 1 | 頁面 2 | 頁面 3 | 頁面 4 | 頁面 5 |
---|---|---|---|---|---|
thrio | 242 | 45 | 39 | 31 | 37 |
boost | 247 | 169 | 196 | 162 | 165 |
固然,thrio 跟 boost 的定位仍是不太同樣的,thrio 更多的偏向於解決咱們業務上的需求,儘可能作到開箱即用。