在早期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 更多的偏向于解决咱们业务上的需求,尽可能作到开箱即用。