thrio 是一个支持 flutter 嵌入原生应用的路由库,目前只有 iOS 版本可看,Android 版本在开发中。
thrio 的诞生主要是为了解决我们自身的业务问题。
我们目前积累了将近 10 万行 Dart 业务代码,早期的时候采用 flutter_boost 提供的解决方案来实现将 Flutter 嵌入原生应用,使用过程中也积累了很多对 flutter_boost 改造的需求,但因为 flutter_boost 的路线图短期或者长期都看不到能满足我们这些需求的可能,所以我们只好自己造了一个轮子。
- 三端统一的打开页面的接口,至少支持 push,支持多开页面实例,flutter_boost 支持
- 三端统一的关闭页面的接口,至少支持关闭顶层页面,关闭特定页面,关闭到特定页面,flutter_boost 支持前两点,第三点不支持
- 三端统一的页面间通知的接口,一定要支持特定页面间的通知传递,flutter_boost 支持不好,无法满足特定页面间的通知传递
- dart 页面导航栏自动隐藏,且不影响原生页面的导航栏,flutter_boost 不支持,需要自行扩展
- iOS 的 FlutterViewController 要支持侧滑返回,flutter_boost 不支持
- 原生页面和 dart 页面要支持页面禁止关闭,flutter_boost 支持了 dart 页面,但页面动画缺失
- 支持 FlutterViewController 内嵌套 Dart 页面,flutter_boost 的支持非常弱
以上对 flutter_boost 的一些判断可能不准确,仅做对比。
- dart 中注册页面路由
class Module with ThrioModule {
@override
void onPageRegister() {
registerPageBuilder(
'flutter3',
(settings) => Page3(index: settings.index, params: settings.params),
);
registerPageBuilder(
'flutter4',
(settings) => Page4(index: settings.index, params: settings.params),
);
}
}
- iOS 中注册页面路由
@interface Module1 : ThrioModule<NavigatorPageObserverProtocol>
@end
@implementation Module1
- (void)onPageRegister {
[self registerPageBuilder:^UIViewController * _Nullable(NSDictionary<NSString *,id> * _Nonnull params) {
UIStoryboard *sb = [UIStoryboard storyboardWithName:@"Main" bundle:nil];
return [sb instantiateViewControllerWithIdentifier:@"ThrioViewController"];
} forUrl:@"native1"];
}
@end
- Android 中注册页面路由
打开操作逻辑需要实现NavigationBuilder
接口
ThrioNavigator.registerNavigationBuilder("native1", object : NavigationBuilder {
override fun getActivityClz(url: String): Class<out Activity> {
return Native2Activity::class.java
}
})
- 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 端打开页面
TODO: poppedResult 暂未实现,回调会超出页面生命周期
ThrioNavigator.push(context, "flutter1", params)
- 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(context, animated)
- 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)
- 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)
给一个页面发送通知,只有当页面呈现之后才会收到该通知。
- dart 端给特定页面发通知
ThrioNavigator.notify(url: 'flutter1', name: 'reload');
- iOS 端给特定页面发通知
[ThrioNavigator notifyUrl:@"flutter1" name:@"reload"];
- Android 端给特定页面发通知
ThrioNavigator.notify(url, index, params)
- dart 端接收页面通知
使用NavigatorPageNotify
这个 Widget 来实现在任何地方接收当前页面收到的通知。
NavigatorPageNotify(
name: 'page1Notify',
onPageNotify: (params) =>
ThrioLogger.v('flutter1 receive notify: $params'),
child: Xxxx());
- iOS 端接收页面通知
UIViewController
实现协议NavigatorPageNotifyProtocol
,通过该协议定义的方法来接收页面通知
- (void)onNotify:(NSString *)name params:(NSDictionary *)params {
ThrioLogV(@"native1 onNotify: %@, %@", name, params);
}
- Android 端接收页面通知
Activity
实现协议OnNotifyListener
,通过 onNotify 回调来接收页面通知
TODO: url, index 需要去除
class Activity : AppCompatActivity(), OnNotifyListener {
override fun onNotify(url: String, index: Int, name: String, params: Any?) {
}
}
实际上实现了 UIViewController 的分类扩展,FlutterViewController 强制设为 YES,原生页面设置导航栏隐藏,也很简单
viewController.thrio_hidesNavigationBar = NO;
FlutterViewController 默认是不支持侧滑返回的,因为 thrio 支持一个 FlutterViewController 可以打开任意多个 dart 页面,dart 页面本身也是要支持侧滑返回的,手势上存在一定的冲突,在这里 thrio 做了一些特殊处理,基本上支持无缝切换。有兴趣可以参看源码实现。
每个端各自维护自身的页面,不支持跨端传递闭包逻辑
- dart 端禁止特定页面关闭
WillPopScope(
onWillPop: () async => true,
child: Container(),
);
- iOS 端禁止特定页面关闭
viewController.thrio_willPopBlock = ^(ThrioBoolCallback _Nonnull result) {
result(NO);
};
一旦设置 thrio_willPopBlock,侧滑返回将失效.
原生端可以获得所有页面的生命周期,Dart 端只能获取自身页面的生命周期
- 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) {}
}
- ios 端获取页面的生命周期
@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,
) {}
}
- ios 端获取页面的路由行为
@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
这是谷歌推荐的实现方式,导航栈中不被原生页面分隔的所有 Flutter 页面共用一个原生的容器页面,有效减少内存消耗量。
- iOS 端
原来的方案是每打开一个 Flutter 页面会创建一个新的原生页面,比如以连续打开 5 个 Flutter 页面计算(比较接近目前我们在项目上的场景),iOS 会消耗 91.67M 内存,新方案 iOS 只消耗 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 |