1. 路由需要做成什么样
写在很前面:
参考上篇(嗯,接近大半年前写的),我们在混合 flutter 工程中通过 flutterboost 来解决引擎问题,但是为了尽可能减少大家调试时间,我们需要让 flutter 工程单独开发调试的时候拥有一个尽可能完整的体验,而不是不跑主工程就没法正常跳转/调试。
碎碎念一下 flutterboost
这里就不介绍这是啥了,相信大家看到咸鱼大佬们的推文应该已经很香了,但是这次的路由很多地方灵感都来源于或者说从 flutterboost 的实现反过来封装的
路由需要点什么:
基础设施
-
- 注册路由:在
flutter单独跑或者混合工程初始化的时候都能正常玩,不需要额外写东西
- 注册路由:在
-
push / pop同1,通过一样的push / pop方法解决
升级版需求
-
- 每个页面都有自己的唯一标识,方便某个地方想要一个页面级的标识符来做些神奇的注册监听等
-
- 生命周期监听
2. 基础设施搭建
依旧是熟悉的 面向协议 / 接口,我们把注册/跳转动作抽象,
以下所有参数里的
code属性都是代表路由名哟,只是以前用的名字懒得改
注册:
由于 flutter 自己的路由注册姿势很多,所以为了把 flutterboost 的注册一起兼容:
typedef Widget PageBuilder(String pageName, Map params, String uniqueId);
///Register a map builders( 这是flutterboost里的注册)
void registerPageBuilders(Map<String, PageBuilder> builders) {
ContainerCoordinator.singleton.registerPageBuilders(builders);
}
uniqueId在注册时候我们直接忽略,因为你其实没办法给每个页面特殊命名,毕竟如果某个页面连续被push多次,我们就没得玩了。
所以 Map<String, PageBuilder> builders 我们把这个当成 register 方法的参数,而其他的其实很简单,所以我们有了下面这个协议定义,当然你也可以把它变成 abstract
typedef Widget RouteBuilder(
String code, Map<dynamic, dynamic> para, String path);
class Router {
void register(Map<String, RouteBuilder> builders) {
throw UnimplementedError("register 方法木有实现哦");
}
// 这是push
Future<Map<dynamic, dynamic>> resolve(String code, Map<dynamic, dynamic> para, BuildContext context) {
throw UnimplementedError("resolve 方法木有实现哦");
}
// 这是push一个url
Future<Map<dynamic, dynamic>> resolvePath(String path, BuildContext context) {
throw UnimplementedError("resolvePath 方法木有实现哦");
}
// 这个你去main里面拿home用得到
TransitionBuilder homeBuilder() {
throw UnimplementedError("homeBuilder 方法木有实现哦");
}
// 这个你去main里面拿home也用得到
Widget homePage() {
throw UnimplementedError("homeBuilder 方法木有实现哦");
}
// 这个是pop
void pop(Map para, BuildContext context) {
throw UnimplementedError("pop 方法木有实现哦");
}
}
RouteBuilder 说简单点就是通过 name / param 构建一个 Widget 页面,我们直接通过这样的形式来创建页面,而不是在 main 里面 onGenerateRoute 的套路,因为这个可以把所有逻辑放到我们的路由模块中来做,也可以兼容到 flutterboost.
基于这个,我们先实现一个 flutter 自己的路由
class GodRouteBox implements Router {
static GodRouteBox shared = GodRouteBox();
Map<String, RouteBuilder> routeMapping = {};
void register(Map<String, RouteBuilder> builders) {
routeMapping = builders;
}
Route routeBuilder(String code, Map<dynamic, dynamic> para) {
RouteBuilder builder = routeMapping[code];
if(builder != null) {
Widget builtPage = builder(code, para, '');
PageRoute route = SlowerPageRoute(name: code, builder: (c) => builtPage);
return route;
} else {
ServiceCenter.logger.error('$code 木有注册过吧', title: '抓到个陌生的路由');
return null;
}
}
Future<Map<dynamic, dynamic>> resolve(String code, Map<dynamic, dynamic> para, BuildContext context) {
RouteBuilder builder = routeMapping[code];
return Navigator.push(context, route);
}
...
void pop(Map para, BuildContext context) {
Navigator.of(context).pop(Map<dynamic, dynamic>.from(para ?? {}));
}
}
为了方便扩展,其实路由文件 SlowerPageRoute 是为了方便扩展而做的,只是为了跳转慢一点而已,
flutter原本的跳转速度在iOS中太快了,一点都不优雅。
class SlowerPageRoute extends MaterialPageRoute<Map<dynamic, dynamic>> {
@override
Duration get transitionDuration => Duration(milliseconds: 600);
SlowerPageRoute({
@required builder,
@required name,
}) : super(builder: builder, settings: RouteSettings(name: name));
}
接下来就很简单了,我们把 flutterboost 也通过这样的形式来包一下
class FlutterBoostRouteBox implements Router {
static FlutterBoostRouteBox shared = FlutterBoostRouteBox();
void register(Map<String, RouteBuilder> builders) {
FlutterBoost.singleton.registerPageBuilders(Map<String, PageBuilder>.from(builders));
}
Future<Map<dynamic, dynamic>> resolve(String code, Map<dynamic, dynamic> para, BuildContext context) async {
return FlutterBoost.singleton.open(code, urlParams: para);
}
void pop(Map para, BuildContext context) {
FlutterBoost.singleton.close(BoostContainer.of(context).settings.uniqueId, result: Map<dynamic, dynamic>.from(para ?? {}));
}
...
}
这些基本在 demo 中都能看得到,就不多写了,
所以使用:
RouteCenter
class RouteCenter {
static Router _route = Global.isSonMode ? FlutterBoostRouteBox.shared : GodRouteBox.shared;
static void register() {
_route.register({
// ==== 路由注册开始 ====
//
FlutterRouteKeys.demo: (pageName, params, _) => MyHomePage(title: 'guagua'),
FlutterRouteKeys.test: (pageName, params, _) => TestPage(id: params['id']),
FlutterRouteKeys.home: (pageName, params, _) => HomePage(),
//
// ==== 路由注册结束 ====
});
}
static Future<Map<dynamic, dynamic>> resolve(
String code, Map<dynamic, dynamic> para, BuildContext context) async {
return _route.resolve(code, para, context);
}
...
}
Global.isSonMode你可以理解成一个开关,true就是儿子模式,即混合模式,false就是flutter单独跑的情况~
这样,只要在应用起来时候调用下 register , MaterialApp 中的首页啥的设置一下,毕竟我们上面都抽象定义过:
void main() async {
RouteCenter.register();
runApp(MyApp());
}
MaterialApp(
title: 'xxxxx',
builder: RouteCenter.homeBuilder,
home: RouteCenter.homePage,
navigatorObservers: obsers,
)
这样之后,God 模式和 Son 模式下都有不同的RouteBox 来实现,都可以正常跳转和工作了
到这里基础兼容就结束了,你一定觉得很简单不就是个封装一个抽象两个实现嘛,嗯确实就是很简单鸭
3. 生命周期
其实这部分是最麻烦的,这部分其实就是兼容了 flutterboost 的实现,首先我们看个需求跳转:
native -> flutter -> native -> flutter -> flutter -> flutter
所以我们的真实跳转方法其实需要的并不都是调用 Flutterboost.open,咸鱼最新的文章也提到,其实之前也提到过,连续的 flutter 页面最好不要通过 flutterboost 来跳转,这样会平白无故增加内存,
所以我们的跳转会变成:
Flutterboost.open -> Flutterboost.open -> Flutterboost.open -> Navigator.push -> Navigator.push -> Navigator.push -> Navigator.push
这样,混合交互时候用 open , flutter 自己交互时候用自己的 push,
接着让人难受的事情又来了,在这样的情况下我们上面设计的路由好像 flutterBoostBox 里接入我们自己的 GodRouteBox 好像还能抢救一下,但是生命周期...
科普个简单的知识:新版的
flutterboost是以FlutterViewContainer容器的模式进行跳转的, 每当Flutterboost.open时候,会出现个新的容器,容器里会承载flutter页面,所以连续两次open, 其实就是创建两个容器,而容器中的flutter页面还可以自由Navigator.push。
所以上面这段文字多看几遍加上在 flutterboost 的 demo 中反复查证后我们发现,Flutterboost.open 我们应该关注容器生命周期,他的交互现象符合我们想要的,而 Navigator.push 我们应该关注页面生命周期,即 flutter 自己的 RouteObserver 系列的实现,
所以其实作为路由架构设计,我们要做的就是把这两者揉在一起给页面上传递正确的结果,而每个业务实现者不应该关注自己应该关注谁的生命周期,他应该只需要得到一个 view appear, view disappear 这样的 生命周期方法重载就行
让 flutter 跳 flutter 先自己解决
我们在注册时候记录下路由表,然后每次跳转时候查一下路由名是不是注册过就行,毕竟 flutter 自己注册的一定是 flutter 页面
class FlutterBoostRouteBox implements Router {
static FlutterBoostRouteBox shared = FlutterBoostRouteBox();
bool isInGodNavigator = false;
Map<String, RouteBuilder> _currentBuilders = {};
void register(Map<String, RouteBuilder> builders) {
GodRouteBox.shared.register(builders);
_currentBuilders = Map<String, PageBuilder>.from(builders);
...
});
Future<Map<dynamic, dynamic>> resolve(String code, Map<dynamic, dynamic> para, BuildContext context) async {
var newPara = para ?? {};
if(_currentBuilders.containsKey(code)) {
var route = GodRouteBox.shared.routeBuilder(code, para);
if(route != null) {
isInGodNavigator = true;
return BoostContainer.of(context).push(route);
}
// newPara['target'] = 'flutter';
}
isInGodNavigator = false;
return FlutterBoost.singleton.open('${Global.basicHost}/$code', urlParams: newPara);
}
void pop(Map para, BuildContext context) {
// pop相对来说简单很多,直接问flutter能不能跳就行了
if(BoostContainer.of(context).canPop()) {
BoostContainer.of(context).pop(Map<dynamic, dynamic>.from(para ?? {}));
return;
}
isInGodNavigator = false;
FlutterBoost.singleton.close(BoostContainer.of(context).settings.uniqueId, result: Map<dynamic, dynamic>.from(para ?? {}));
}
}
很高兴的翻出来
BoostContainer这样个自定义的Navigator,然而发现,他只是个自定义的而已,对我们的需求没任何帮助...
用搭 基础设施 的思路来做一个最简单的路由监听的
假设你已经了解 routeObserver 和 RouteAware 的套路,但是这是依赖于 flutter 自己的 ,所以我们基于这个来扩展成可以支持除了 flutter 自己的,也能支持花里胡哨的 flutterboost
我们又YY出一个协议/接口定义:
mixin RouteTracker {
onPushed(String route);
willPop(String route);
onPopped(String route);
onDismissed(String route);
}
class CommonRouteObserver {
void subscribe(RouteTracker tracker, BuildContext context) {
}
void unSubscribe(RouteTracker tracker, BuildContext context) {
}
}
是不是和 RouteObserver 和 RouteAware 的思路很像,其实也是抄作业(抄灵感),这样,我们就可以建一个 GodObserverBox,来把原本 RouteObserver 的逻辑放进去了:
先是 RouteAware
class RouteAwareWrapper extends RouteAware {
final RouteTracker routeTracker;
final String routeId;
RouteAwareWrapper({@required this.routeTracker, @required this.routeId});
@override
void didPopNext() {
// ServiceCenter.logger.print('路由嘿嘿 =》$routeId ===> didPopNext}');
routeTracker?.onPopped(routeId);
}
@override
void didPush() {
// ServiceCenter.logger.print('路由嘿嘿 =》$routeId ===> didPush}');
routeTracker?.onPushed(routeId);
}
@override
void didPop() {
// ServiceCenter.logger.print('路由嘿嘿 =》$routeId ===> didPop}');
routeTracker?.onDismissed(routeId);
}
@override
void didPushNext() {
}
}
然后就是 GodRouteObserver
final RouteObserver routeObserver = RouteObserver();
class GodGlobalRouteObserver implements CommonRouteObserver {
Map<String, RouteAwareWrapper> _routeWrappers = {};
static GodGlobalRouteObserver shared = GodGlobalRouteObserver();
void subscribe(RouteTracker tracker, BuildContext context) {
String lastId = routeObserver.lastSettingUniqueId ?? '';
if(_routeWrappers.containsKey(lastId) || lastId.isEmpty) {
return;
}
RouteAwareWrapper wrapper = RouteAwareWrapper(routeId: routeObserver.lastSettingUniqueId,
routeTracker: tracker);
_routeWrappers[lastId] = wrapper;
routeObserver.subscribe(wrapper, ModalRoute.of(context));
}
void unSubscribe(RouteTracker tracker, BuildContext context) {
String lastId = routeObserver.lastSettingUniqueId;
RouteAwareWrapper wrapper = _routeWrappers[lastId];
if(wrapper != null) {
routeObserver.unsubscribe(wrapper);
_routeWrappers.remove(lastId);
}
}
...
}
我们需要做一个大胆的假设:
routeObserver.lastSettingUniqueId就是我们之前提到过的页面唯一标识,假设我们已经实现了这个,后面会具体说怎么实现,现在我们需要通过这个id来进行监听的注册管理,
这里有个小 tips :
flutter本身的routeObserver的路由监听主key是route, 而不是一个唯一id,所以其实并不安全是可能存在连续跳转几个相同界面后,监听出现错乱的情况的,亲测~
同样的道理,我们在 flutterboost 侧也实现一个:
class FlutterBoostRouteObserver implements CommonRouteObserver {
static FlutterBoostRouteObserver shared = FlutterBoostRouteObserver();
Map<String, RouteTracker> _routeTracker = {};
void subscribe(RouteTracker tracker, BuildContext context) {
BoostContainerSettings settings = BoostContainer.of(context).settings;
_routeTracker[settings.uniqueId] = tracker;
}
void unSubscribe(RouteTracker tracker, BuildContext context) {
BoostContainerSettings settings = BoostContainer.of(context).settings;
if(_routeTracker.containsKey(settings.uniqueId)) {
_routeTracker.remove(settings.uniqueId);
}
}
}
好了接下来我们加让 flutterboost 的容器周期来先简单实现些这些:
FlutterBoostRouteObserver(){
setup();
}
setup() {
FlutterBoost.singleton.addBoostContainerLifeCycleObserver((ContainerLifeCycle state, BoostContainerSettings settings){
if(state == ContainerLifeCycle.Background || state == ContainerLifeCycle.Foreground) {
return;
}
var name = settings.uniqueId;
var storedState = lifeCycleMap[name];
if(storedState == null) {
lifeCycleMap[name] = state;
return;
}
switch(state) {
case ContainerLifeCycle.Destroy:
onDismissed(name);
break;
case ContainerLifeCycle.Appear:
if(storedState == ContainerLifeCycle.Init) {
onPushed(name);
}
if(storedState == ContainerLifeCycle.Disappear ){
onPopped(name);
}
break;
default:
break;
}
lifeCycleMap[name] = state;
});
}
这个只是简单的逻辑判断,比如容器出现消失摧毁分别对应页面的什么样的情况,就不多解释了,这样以后我们就把两个 RouteObserverBox 都装好了,然后就是在 RouteCenter 中的定义了:
class RouteCenter {
...
// 这是给main用的
static List<NavigatorObserver> routeObservers = Global.isSonMode ? [] : [routeObserver];
// 这是给也无用的
static CommonRouteObserver globalObserver = Global.isSonMode ? FlutterBoostRouteObserver.shared : GodGlobalRouteObserver.shared;
...
}
我们普通页面的注册:
class Test extends State<Test> with RouteTracker {
@override
void didChangeDependencies() {
super.didChangeDependencies();
RouteCenter.globalObserver.subscribe(this, context);
}
@override
onPushed(String route) {
ServiceCenter.logger.print('路由噜噜 =》$route ===> onPushed}');
}
...
}
到这里为止,其实两种路由监听也就能正常运转了
在混合跳转中让两种 routeObserverBox 混在一起用
class FlutterBoostRouteObserver implements CommonRouteObserver {
;;;
void subscribe(RouteTracker tracker, BuildContext context) {
GodGlobalRouteObserver.shared.subscribe(tracker, context);
BoostContainerSettings settings = BoostContainer.of(context).settings;
_routeTracker[settings.uniqueId] = tracker;
}
}
在我们上面做了两个盒子之后,我们在 subscribe 的过程中区分注册就行了,但是为了保证flutter 自己路由是正常监听的,我们让 FlutterBoostRouteObserver 同时注册我们自己的 GodGlobalRouteObserver,这样保证不会丢生命周期,但是会出现两个监听多触发的情况,根据实际情况筛选掉就可以啦
其实我们发现,这样之后就能支持穿插着的跳转了,很神奇吧
所以我们接下来把上面说的 UniqueId 的功能,让每次路由跳转都能有个单独的页面id 他可以用来处理很多神奇的事,
接下来需要处理这个:
UniqueId
之前我们提过每个页面需要有个唯一标识,其实原理很简单,我们自定义个 Route 和 Setting ,这样跳转时候我们可以拿到 Route 和 Settings,就能正常判断了:
class SlowerPageRoute extends MaterialPageRoute<Map<dynamic, dynamic>> {
@override
// TODO: implement transitionDuration
Duration get transitionDuration => Duration(milliseconds: 1000);
SlowerPageRoute({
@required builder,
@required name,
}) : super(builder: builder, settings: UniqueRouteSettings.buildUnique(name));
UniqueRouteSettings get uniqueSetting => settings;
}
class UniqueRouteSettings extends RouteSettings {
static int _uniqueId = 10000;
final String uniqueId;
UniqueRouteSettings({@required String name,
@required this.uniqueId}) : super(name: name);
UniqueRouteSettings.buildUnique(String name) : uniqueId = RouteCenter.topSharedRouteId ?? _uniqueId.toString(), super(name: name) {
_uniqueId += 1;
}
}
Rouete 用法在 GodRouteBox 中一样,就不多啰嗦了
自己定一个 RouteObserver ,来记录跳转路由
extension UniqueRoute on Route {
SlowerPageRoute get slowPageRoute => this is SlowerPageRoute ? this : null;
String get uniqueSetting => slowPageRoute?.uniqueSetting?.uniqueId ?? null;
}
class GodRouteObserver extends RouteObserver {
// 用于记录 SlowerPageRoute
List<SlowerPageRoute> _routes = [];
// 用来记录 非SlowerPageRoute, 其实就是所谓的初号页面
List<Route> _unknownRoutes = [];
String lastSettingUniqueId = '';
@override
void didPush(Route route, Route previousRoute) {
super.didPush(route, previousRoute);
SlowerPageRoute current = route.slowPageRoute;
if(current == null) {
// 去flutterboostBox里挖出来那个route
// 把那个Id抓出来给 lastSettingUniqueId
// ...
// lastSettingUniqueId = id
_unknownRoutes.add(route);
} else {
lastSettingUniqueId = current.uniqueSetting.uniqueId;
_routes.add(route);
}
}
}
目前可能出现的页面以及路由对应是这样的:
native- flutter(初号) -flutter - flutter
native- BoostPageRoute -SlowPageRoute - SlowPageRoute,
所以其实 初号flutter 页面中的 uniqueId 需要 flutterBoostOvserver 生成id 把 BoostPageRoute.uniqueId 给 GodRouteObserver 就行了。
为了让
GodRouteObserver保持干净,我们当然不可能吧boost引进来,所以:
abstract class RouteAnalyzer {
String uniqueIdFromRoute(Route route);
}
class GodRouteObserver extends RouteObserver {
List<RouteAnalyzer> _analyzers = [];
void addAnalyzer(RouteAnalyzer analyzer) {
_analyzers.add(analyzer);
}
@override
void didPush(Route route, Route previousRoute) {
super.didPush(route, previousRoute);
SlowerPageRoute current = route.slowPageRoute;
if(current == null) {
String routeId;
for(var analyzer in _analyzers) {
routeId = analyzer.uniqueIdFromRoute(route);
if(routeId != null) {
lastSettingUniqueId = routeId;
_unknownRoutes.add(route);
break;
}
}
} else {
lastSettingUniqueId = current.uniqueSetting.uniqueId;
_routes.add(route);
}
}
...
}
最终形态是这样的,我们让GodRouteObserver 暴露个 _analyzers 出来,这样就能让别的 RouteObserver 给 GodRouteObserver 来解析不一样的 Route了
比如:
class BoostBoxRouteAnalyzer extends RouteAnalyzer {
String uniqueIdFromRoute(Route route) {
if(route is boostPage.BoostPageRoute) {
boostPage.BoostPageRoute pageRoute = route;
return pageRoute.uniqueId;
} else {
return null;
}
}
}
然后
routeObserver.addAnalyzer(BoostBoxRouteAnalyzer());
当然别忘了:
FlutterBoost.singleton.addBoostNavigatorObserver(routeObserver);
就行啦
最终我们的 topUniqueId
class FlutterBoostRouteBox implements Router {
static FlutterBoostRouteBox shared = FlutterBoostRouteBox();
String get topRouteId => isInGodNavigator ? GodRouteBox.shared.topRouteId : FlutterBoost.singleton.containerManagerKey.currentState.onstageSettings.uniqueId;
...
}
class GodRouteBox implements Router {
static GodRouteBox shared = GodRouteBox();
String get topRouteId => routeObserver.lastSettingUniqueId;
...
}
在 RouteCenter 中我们就能获取到真实混合的 uniqueId 了
class RouteCenter {
static Router _route = Global.isSonMode ? FlutterBoostRouteBox.shared : GodRouteBox.shared;
...
static String get topRouteId => _route.topRouteId ?? '-99';
...
}
终于写完了,可能有点长又复杂,其实宗旨就是把原生flutter 和 boost 捏到一起让他们能在不同模式下正常跑起来。
神奇的
flutter架构设计还会继续,虽然之前断档了很久,但是应该不会缺席啦,敬请期待~
最后贴个广告:
我在宜家,万物皆宜的宜,欢迎回家的家
