【抄作业提高】怎么样把 Flutterboost 和原生路由结合起来玩的很胖很有意思

1,796 阅读7分钟

1. 路由需要做成什么样

写在很前面:

参考上篇(嗯,接近大半年前写的),我们在混合 flutter 工程中通过 flutterboost 来解决引擎问题,但是为了尽可能减少大家调试时间,我们需要让 flutter 工程单独开发调试的时候拥有一个尽可能完整的体验,而不是不跑主工程就没法正常跳转/调试。

碎碎念一下 flutterboost

这里就不介绍这是啥了,相信大家看到咸鱼大佬们的推文应该已经很香了,但是这次的路由很多地方灵感都来源于或者说从 flutterboost 的实现反过来封装的

路由需要点什么:

基础设施

    1. 注册路由:在 flutter 单独跑或者混合工程初始化的时候都能正常玩,不需要额外写东西
    1. push / pop 同1,通过一样的push / pop 方法解决

升级版需求

    1. 每个页面都有自己的唯一标识,方便某个地方想要一个页面级的标识符来做些神奇的注册监听等
    1. 生命周期监听

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 单独跑的情况~

这样,只要在应用起来时候调用下 registerMaterialApp 中的首页啥的设置一下,毕竟我们上面都抽象定义过:

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

所以上面这段文字多看几遍加上在 flutterboostdemo 中反复查证后我们发现,Flutterboost.open 我们应该关注容器生命周期,他的交互现象符合我们想要的,而 Navigator.push 我们应该关注页面生命周期,即 flutter 自己的 RouteObserver 系列的实现,

所以其实作为路由架构设计,我们要做的就是把这两者揉在一起给页面上传递正确的结果,而每个业务实现者不应该关注自己应该关注谁的生命周期,他应该只需要得到一个 view appear, view disappear 这样的 生命周期方法重载就行


flutterflutter 先自己解决

我们在注册时候记录下路由表,然后每次跳转时候查一下路由名是不是注册过就行,毕竟 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,然而发现,他只是个自定义的而已,对我们的需求没任何帮助...

用搭 基础设施 的思路来做一个最简单的路由监听的

假设你已经了解 routeObserverRouteAware 的套路,但是这是依赖于 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) {

  }
}

是不是和 RouteObserverRouteAware 的思路很像,其实也是抄作业(抄灵感),这样,我们就可以建一个 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 的路由监听主 keyroute, 而不是一个唯一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

之前我们提过每个页面需要有个唯一标识,其实原理很简单,我们自定义个 RouteSetting ,这样跳转时候我们可以拿到 RouteSettings,就能正常判断了:

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.uniqueIdGodRouteObserver 就行了。

为了让 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 出来,这样就能让别的 RouteObserverGodRouteObserver 来解析不一样的 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';
  ...
 }

终于写完了,可能有点长又复杂,其实宗旨就是把原生flutterboost 捏到一起让他们能在不同模式下正常跑起来。

神奇的 flutter 架构设计还会继续,虽然之前断档了很久,但是应该不会缺席啦,敬请期待~


最后贴个广告:

我在宜家,万物皆宜的宜,欢迎回家的家

宜家 app 刚上线,有好多好玩的东西等待你一起挖掘创新哟

s