如何无缝的将Flutter引入现有应用?
为什么写thrio?
在早期Flutter发布的时候,谷歌虽然提供了iOS和Android App上的Flutter嵌入方案,但主要针对的是纯Flutter的情形,混合开发支持的并不友好。
所谓的纯RN、纯weex应用的生命周期都不存在,所以也不会存在一个纯Flutter的App的生命周期,因为我们总是有需要复用现有模块。
所以我们需要一套足够完整的Flutter嵌入原生App的路由解决方案,所以我们自己造了个轮子 ,现已开源,遵循MIT协议。
thrio的设计原则
原则一,dart端最小改动接入
原则二,原生端最小侵入
原则三,三端保持一致的API
thrio所有功能的设计,都会遵守这三个原则。下面会逐步对功能层面一步步展开进行说明,后面也会有原理性的解析。
thrio的页面路由
以dart中的 为主要参照,提供以下路由能力:
push,打开一个页面并放到路由栈顶
pop,关闭路由栈顶的页面
popTo,关闭到某一个页面
remove,删除任意页面
Navigator中的API几乎都可以通过组合以上方法实现, 方法暂未提供。
不提供iOS中存在的 功能,因为会导致原生路由栈被覆盖,维护复杂度会非常高,如确实需要可以通过修改转场动画实现。
页面的索引
要路由,我们需要对页面建立索引,通常情况下,我们只需要给每个页面设定一个 就可以了,如果每个页面都只打开一次的话,不会有任何问题。但是当一个页面被打开多次之后,仅仅通过url是无法定位到明确的页面实例的,所以在 中我们增加了页面索引的概念,具体在API中都会以 来表示,同一个url第一个打开的页面的索引为 ,之后同一个 的索引不断累加。
如此,唯一定位一个页面的方式为 + ,在dart中 的 就是由 组合而成。
很多时候,使用者不需要关注 ,只有当需要定位到多开的 的页面中的某一个时才需要关注 。最简单获取 的方式为 方法的回调返回值。
页面的push
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")
}
)
dart端只需要await push,就可以连续打开页面
原生端需要等待push的result回调返回才能打开第二个页面
三端都可以通过闭包 poppedResult 来获取
页面的pop
// 默认动画开启
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)
页面的popTo
// 默认动画开启
ThrioNavigator.popTo(url: 'flutter1');
// 不开启动画,原生和dart页面都生效
ThrioNavigator.popTo(url: 'flutter1', animated: false);
// 默认动画开启
[ThrioNavigator popToUrl:@"flutter1"];
// 关闭动画
[ThrioNavigator popToUrl:@"flutter1" animated:NO];
ThrioNavigator.popTo(context, url, index)
页面的remove
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的一个必备能力被引入了thrio。
发送页面通知
ThrioNavigator.notify(url: 'flutter1', name: 'reload');
[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
ThrioNavigator.notify(url, index, params)
接收页面通知
使用 这个 来实现在任何地方接收当前页面收到的通知。
NavigatorPageNotify(
name: 'page1Notify',
onPageNotify: (params) =>
ThrioLogger.v('flutter1 receive notify: $params'),
child: Xxxx());
实现协议,通过 来接收页面通知
- (void)onNotify:(NSString )name params:(NSDictionary )params {
ThrioLogV(@"native1 onNotify: %@, %@", name, params);
}
实现协议,通过 来接收页面通知
class Activity : AppCompatActivity(), OnNotifyListener {
override fun onNotify(name: String, params: Any?) {
}
}
因为Android activity在后台可能会被销毁,所以页面通知实现了一个懒响应的行为,只有当页面呈现之后才会收到该通知,这也符合页面需要刷新的场景。
thrio的模块化
模块化在thrio里面只是一个非核心功能,仅仅为了实现原则二而引入原生端。
thrio的模块化能力由一个类提供,,很小巧,主要提供了 的注册链和初始化链,让代码可以根据路由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 端只能获取自身页面的生命周期
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
@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 页面的路由行为
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
@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 中,有一个 提供了该功能,thrio 完好的保留了这个功能。
WillPopScope(
onWillPop: () async => true,
child: Container(),
);
在 iOS 中,thrio 提供了类似的功能,返回 表示不会关闭,一旦设置会将侧滑返回手势禁用
viewController.thriowillPopBlock = ^(ThrioBoolCallback Nonnull result) {
result(NO);
};
关于 的侧滑返回手势,Flutter 默认支持的是纯Flutter应用,仅支持单一的 作为整个App的容器,内部已经将 的侧滑返回手势去掉。但 thrio 要解决的是 Flutter 与原生应用的无缝集成,所以必须将侧滑返回的手势加回来。
thrio的设计解析
目前开源 Flutter 嵌入原生的库,主要的还是通过切换 FlutterEngine 上的原生容器来实现的,这是 Flutter 原本提供的原生容器之上最小改动而实现,需要小心处理好容器切换的时序,否则在页面导航时会产生崩溃。基于 Flutter 提供的这个功能, thrio 构建了三端一致的页面管理API。
dart 的核心类
dart 端只管理 dart页面
name = url.index
isInitialRoute = !isNested
arguments = params
主要提供页面描述和转场动画的是否配置的功能
Future push(RouteSettings settings, {
bool animated = true,
NavigatorParamsCallback poppedResult,
});
Future pop(RouteSettings settings, {bool animated = true});
Future popTo(RouteSettings settings, {bool animated = true});
Future remove(RouteSettings settings, {bool animated = false});
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 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 notify({
@required String url,
int index,
@required String name,
params,
});
/// Pop a page from the navigation stack.
///
static Future pop({params, bool animated = true})
static Future popTo({
@required String url,
int index,
bool animated = true,
});
/// Remove the page with url in the navigation stack.
///
static Future remove({
@required String url,
int index,
bool animated = true,
});
}
iOS 的核心类
@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
存储通知、页面关闭回调、NavigatorRouteSettings
route的双向链表
提供一些列的路由内部接口
并能兼容非 thrio 体系内的页面
提供 容器上的 dart 页面的管理功能
提供 popDisable 等功能
@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 路由栈的结构

dart 与 iOS push的时序图

dart 与 iOS pop的时序图

dart 与 iOS popTo的时序图

dart 与 iOS remove的时序图

总结
目前 Flutter 接入原生应用主流的解决方案应该是boost,https://github.com/alibaba/flutter_boost,笔者的团队在项目深度使用过 boost,也积累了很多对 boost 改善的需求,遇到的最大问题是内存问题,每打开一个 Flutter 页面的内存开销基本到了很难接受的程度,thrio,https://github.com/hellobike/thrio 把解决内存问题作为头等任务,最终效果还是不错的,比如以连续打开 5 个 Flutter 页面计算,boost 的方案会消耗 91.67M 内存,thrio 只消耗 42.76 内存,模拟器上跑出来的数据大致如下:

同样连续打开 5 个页面的场景,thrio 打开第一个页面跟 boost 耗时是一样的,因为都需要打开一个新的 Activity,之后 4 个页面 thrio 会直接打开 Flutter 页面,耗时会降下来,以下单位为 ms:

当然,thrio 跟 boost 的定位还是不太一样的,thrio 更多的偏向于解决我们业务上的需求,尽量做到开箱即用。