Flutter框架《get》的路由管理解析

  • 概述

    get的官方文档上介绍说,它具有更快和更实际的路由管理,至于性能上是不是如他所说我暂时没做比较,本文从初始化的路由逻辑和部分其他跳转逻辑的代码上来看一下和Flutter原生跳转有何不同,确切地说是做了何种封装。

  • Flutter原生初始路由获取

    在Flutter原生中,初始化路由主要通过在MaterialApp中指定的initialRoute和routes属性来设置初始页面,当然也可以设置home属性。

    在_WidgetsAppState的build方法中有这么一段:

    Widget? routing;
    if (_usesRouter) {
      assert(_effectiveRouteInformationProvider != null);
      routing = Router<Object>(
        routeInformationProvider: _effectiveRouteInformationProvider,
        routeInformationParser: widget.routeInformationParser,
        routerDelegate: widget.routerDelegate!,
        backButtonDispatcher: widget.backButtonDispatcher,
      );
    } else if (_usesNavigator) {
      assert(_navigator != null);
      routing = Navigator(
        restorationScopeId: 'nav',
        key: _navigator,
        initialRoute: _initialRouteName,
        onGenerateRoute: _onGenerateRoute,
        onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
          ? Navigator.defaultGenerateInitialRoutes
          : (NavigatorState navigator, String initialRouteName) {
            return widget.onGenerateInitialRoutes!(initialRouteName);
          },
        onUnknownRoute: _onUnknownRoute,
        observers: widget.navigatorObservers!,
        reportsRouteUpdateToEngine: true,
      );
    }
    

    可见,首先会判断_usesRouter来决定routing,那他是什么呢?

    bool get _usesRouter => widget.routerDelegate != null;
    bool get _usesNavigator => widget.home != null || widget.routes?.isNotEmpty == true || widget.onGenerateRoute != null || widget.onUnknownRoute != null;
    

    可以看到,首先会先采用routerDelegate来创建route,其次会根据home、routes、onGenerateRoute或onUnknownRoute来生成route。

    以Navigator为例,它的initialRoute属性通过 _initialRouteName来赋值, _initialRouteName产生逻辑如下:

    String get _initialRouteName => WidgetsBinding.instance!.window.defaultRouteName != Navigator.defaultRouteName
      ? WidgetsBinding.instance!.window.defaultRouteName
      : widget.initialRoute ?? WidgetsBinding.instance!.window.defaultRouteName;
    

    WidgetsBinding.instance!.window.defaultRouteName是Android原生API传过来的initialRoute,这个会首先被尝试取用,如果没设置则取MaterialApp设置的initialRoute属性的值。

    initialRoute流程中也会走到_onGenerateRoute方法:

    Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
      final String? name = settings.name;
      final WidgetBuilder? pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
          ? (BuildContext context) => widget.home!
          : widget.routes![name];
    
      if (pageContentBuilder != null) {
        assert(
          widget.pageRouteBuilder != null,
          'The default onGenerateRoute handler for WidgetsApp must have a '
          'pageRouteBuilder set if the home or routes properties are set.',
        );
        final Route<dynamic> route = widget.pageRouteBuilder!<dynamic>(
          settings,
          pageContentBuilder,
        );
        assert(route != null, 'The pageRouteBuilder for WidgetsApp must return a valid non-null Route.');
        return route;
      }
      if (widget.onGenerateRoute != null)
        return widget.onGenerateRoute!(settings);
      return null;
    }
    

    可见,在这里会决定是直接用home还是去routes中查找,总结一下就是,如果initialRoute没有设置并且设置了home的情况下会使用home,否则就尝试去routes中取。

  • get 框架初始路由的获取

    和原生不同的是,get框架用的是getPages属性,原生的routes属性是一个map,而getPages属性是一个list。

    GetMaterialApp的build方法中有一句:

    if (getPages != null) {
      Get.addPages(getPages!);
    }
    

    Get.addPages方法如下:

    void addPages(List<GetPage> getPages) {
      routeTree.addRoutes(getPages);
    }
    

    好了下面我们来看使用。

    NavigatorState的build方法如下:

    @override
    Widget build(BuildContext context) {
      assert(!_debugLocked);
      assert(_history.isNotEmpty);
      // Hides the HeroControllerScope for the widget subtree so that the other
      // nested navigator underneath will not pick up the hero controller above
      // this level.
      return HeroControllerScope.none(
        child: Listener(
          onPointerDown: _handlePointerDown,
          onPointerUp: _handlePointerUpOrCancel,
          onPointerCancel: _handlePointerUpOrCancel,
          child: AbsorbPointer(
            absorbing: false, // it&#39;s mutated directly by _cancelActivePointers above
            child: FocusScope(
              node: focusScopeNode,
              autofocus: true,
              child: UnmanagedRestorationScope(
                bucket: bucket,
                child: Overlay(
                  key: _overlayKey,
                  initialEntries: overlay == null ?  _allRouteOverlayEntries.toList(growable: false) : const &lt;OverlayEntry&gt;[],
                ),
              ),
            ),
          ),
        ),
      );
    }
    

    可以看到,最内层的child是Overlay,它的State的build方法如下:

    @override
    Widget build(BuildContext context) {
      // This list is filled backwards and then reversed below before
      // it is added to the tree.
      final List<Widget> children = <Widget>[];
      bool onstage = true;
      int onstageCount = 0;
      for (int i = _entries.length - 1; i >= 0; i -= 1) {
        final OverlayEntry entry = _entries[i];
        if (onstage) {
          onstageCount += 1;
          children.add(_OverlayEntryWidget(
            key: entry._key,
            entry: entry,
          ));
          if (entry.opaque)
            onstage = false;
        } else if (entry.maintainState) {
          children.add(_OverlayEntryWidget(
            key: entry._key,
            entry: entry,
            tickerEnabled: false,
          ));
        }
      }
      return _Theatre(
        skipCount: children.length - onstageCount,
        children: children.reversed.toList(growable: false),
        clipBehavior: widget.clipBehavior,
      );
    }
    

    这里是含有多个child,每一个都是_OverlayEntryWidget,它的State的build 方法如下:

    @override
    Widget build(BuildContext context) {
      return TickerMode(
        enabled: widget.tickerEnabled,
        child: widget.entry.builder(context),
      );
    }
    

    可以看到,这里最终调用了entry的builder函数来创建Widget,那这个entry从哪来的呢?它来自前面NavigatorState的build方法中传入的initialEntries属性的值,_allRouteOverlayEntries根据 _history生成:

    Iterable<OverlayEntry> get _allRouteOverlayEntries sync* {
      for (final _RouteEntry entry in _history)
        yield* entry.route.overlayEntries;
    }
    

    而_history又是在NavigatorState的restoreState方法(这个方法至少会在initState之后调用一次)中添加的initialRoute:

    if (initialRoute != null) {
      _history.addAll(
        widget.onGenerateInitialRoutes(
          this,
          widget.initialRoute ?? Navigator.defaultRouteName,
        ).map((Route<dynamic> route) => _RouteEntry(
            route,
            initialState: _RouteLifecycle.add,
            restorationInformation: route.settings.name != null
              ? _RestorationInformation.named(
                name: route.settings.name!,
                arguments: null,
                restorationScopeId: _nextPagelessRestorationScopeId,
              )
              : null,
          ),
        ),
      );
    }
    

    可见,entry就是这里的_RouteEntry,entry.route就是这里的route,它是由widget.onGenerateInitialRoutes创建的,回到Navigator的构造处,widget.onGenerateInitialRoutes就是:

    (NavigatorState navigator, String initialRouteName) {
        return widget.onGenerateInitialRoutes!(initialRouteName);
    }
    

    这里其实又会调用Navigator的widget的onGenerateInitialRoutes函数,回到GetMaterialApp的build方法中,返回的MaterialApp构造对象的onGenerateInitialRoutes属性为:

    onGenerateInitialRoutes: (getPages == null || home != null)
        ? onGenerateInitialRoutes
        : initialRoutesGenerate,
    

    因为此处getPages不为null且没有配置home属性,所以用的是initialRoutesGenerate函数:

    List<Route<dynamic>> initialRoutesGenerate(String name) {
      return [
        PageRedirect(
          settings: RouteSettings(name: name),
          unknownRoute: unknownRoute,
        ).page()
      ];
    }
    

    所以上面的overlayEntries来自于这里产生的Route,经查发现,overlayEntries定义于OverlayRoute中,它们的继承关系是GetPageRoute->PageRoute->ModalRoute->TransitionRoute->OverlayRoute->Route。overlayEntries返回的就是_overlayEntries的值, _overlayEntries会在install方法中添加内容,install方法在Route插入到Navigator时会被调用:

    @factory
    Iterable<OverlayEntry> createOverlayEntries();
    
    @override
    List<OverlayEntry> get overlayEntries => _overlayEntries;
    final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
    
    @override
    void install() {
      assert(_overlayEntries.isEmpty);
      _overlayEntries.addAll(createOverlayEntries());
      super.install();
    }
    

    可见,overlayEntries的内容来自于createOverlayEntries方法,这个方法交给子类重写,根据继承关系我们在ModalRoute中找到了该方法的实现:

    @override
    Iterable<OverlayEntry> createOverlayEntries() sync* {
      yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
      yield _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
    }
    

    这里会使用生成器来创建OverlayEntry,为什么一次生成两个呢?_modalBarrier其实是页面之间的隔断,相当于分割线的角色。

    所以,_OverlayEntryWidgetState的build中entry的builder函数就是这里的 _buildModalBarrier和 _buildModalScope, _buildModalScope如下:

    Widget _buildModalScope(BuildContext context) {
      // To be sorted before the _modalBarrier.
      return _modalScopeCache ??= Semantics(
        sortKey: const OrdinalSortKey(0.0),
        child: _ModalScope<T>(
          key: _scopeKey,
          route: this,
          // _ModalScope calls buildTransitions() and buildChild(), defined above
        ),
      );
    }
    

    在_ModalScope的State的build中的内层child是:

    child: _page ??= RepaintBoundary(
      key: widget.route._subtreeKey, // immutable
      child: Builder(
        builder: (BuildContext context) {
          return widget.route.buildPage(
            context,
            widget.route.animation!,
            widget.route.secondaryAnimation!,
          );
        },
      ),
    ),
    

    widget.route是前面传入的‘this’,也就是ModalRoute本身,在GetPageRoute继承自ModalRoute的子类链中我们并没有发现有实现这个方法,我们最终在GetPageRouteTransitionMixin中找到了实现:

    mixin GetPageRouteTransitionMixin<T> on PageRoute<T>{
      @protected
      Widget buildContent(BuildContext context);
    
      @override
      Widget buildPage(BuildContext context, Animation<double> animation,
          Animation<double> secondaryAnimation) {
        final child = buildContent(context);
        final Widget result = Semantics(
          scopesRoute: true,
          explicitChildNodes: true,
          child: child,
        );
        return result;
      }
    }
    

    buildPage返回了Semantics,它的child是通过buildContent创建的,这个方法在GetPageRoute中实现:

    @override
    Widget buildContent(BuildContext context) {
      return _getChild();
    }
    

    _getChild方法如下:

    Widget _getChild() {
      if (_child != null) return _child!;
      final middlewareRunner = MiddlewareRunner(middlewares);
    
      final localbindings = [
        if (bindings != null) ...bindings!,
        if (binding != null) ...[binding!]
      ];
      final bindingsToBind = middlewareRunner.runOnBindingsStart(localbindings);
      if (bindingsToBind != null) {
        for (final binding in bindingsToBind) {
          binding.dependencies();
        }
      }
    
      final pageToBuild = middlewareRunner.runOnPageBuildStart(page)!;
      _child = middlewareRunner.runOnPageBuilt(pageToBuild());
      return _child!;
    }
    

    这个地方_child的构造和很多尚未确定的变量有关,所以这里有些麻烦,一步一步来找。

    _child通过middlewareRunner.runOnPageBuilt产生,runOnPageBuilt方法的参数是一个函数,通过middlewareRunner.runOnPageBuildStart产生,runOnPageBuildStart方法和runOnPageBuilt方法内部逻辑都是会尝试调用 _getMiddlewares方法循环每一个中间件,最后采用最后一个中间件(这个东西有什么用现在还不知道)的对应方法处理过的参数:

    GetPageBuilder? runOnPageBuildStart(GetPageBuilder? page) {
      _getMiddlewares().forEach((element) {
        page = element.onPageBuildStart(page);
      });
      return page;
    }
    
    Widget runOnPageBuilt(Widget page) {
      _getMiddlewares().forEach((element) {
        page = element.onPageBuilt(page);
      });
      return page;
    }
    

    那么_getMiddlewares方法获取的是什么呢?是 _middlewares,他在构造时传入,也就是前面 _getChild方法传入的middlewares,它又是通过GetPageRoute构造时传入的,也就是在PageRedirect的page方法中,middlewares的值是 _r.middlewares, _r是:

    final _r = (isUnknown ? unknownRoute : route)!;
    

    这里的isUnknown是false,所以_r就是 route,route是什么时候构造的呢?在page方法的最开始有一句:

    while (needRecheck()) {}
    

    needRecheck方法如下:

    bool needRecheck() {
      if (settings == null && route != null) {
        settings = route;
      }
      final match = Get.routeTree.matchRoute(settings!.name!);
      Get.parameters = match.parameters;
    
      // No Match found
      if (match.route == null) {
        isUnknown = true;
        return false;
      }
    
      final runner = MiddlewareRunner(match.route!.middlewares);
      route = runner.runOnPageCalled(match.route);
      addPageParameter(route!);
    
      // No middlewares found return match.
      if (match.route!.middlewares == null || match.route!.middlewares!.isEmpty) {
        return false;
      }
      final newSettings = runner.runRedirect(settings!.name);
      if (newSettings == null) {
        return false;
      }
      settings = newSettings;
      return true;
    }
    

    还记得我们之前把getPages属性的所有GetPage都存在了Get.routeTree吗,这里就是去那里找到initialRoute指定的初始路由页面,所以middlewares是在getPages路由集合里构造每一个GetPage时自定义设置的,所以到这里我们也就知道了middlewares的作用,就是在构造page之前提供一个额外的处理入口,通过它你可以做一些自定义的处理工作。

    所以_getChild 方法中的page就是GetPage中page属性指向的值,也就是我们的页面,比如:

    GetPage unknowPage = GetPage(
      name: "myPage",
      page: () => MyPage(),
    );
    

    如上,MyPage是我们自定义的页面组件,在这个流程中最终会最为最内层的组件显示在最上面。

  • get 框架的部分其他路由方法

    以Get.to方法为例:

    Future<T?>? to<T>(
      dynamic page, {
      bool? opaque,
      Transition? transition,
      Curve? curve,
      Duration? duration,
      int? id,
      String? routeName,
      bool fullscreenDialog = false,
      dynamic arguments,
      Bindings? binding,
      bool preventDuplicates = true,
      bool? popGesture,
      double Function(BuildContext context)? gestureWidth,
    }) {
      // var routeName = "/${page.runtimeType}";
      routeName ??= "/${page.runtimeType}";
      routeName = _cleanRouteName(routeName);
      if (preventDuplicates && routeName == currentRoute) {
        return null;
      }
      return global(id).currentState?.push<T>(
            GetPageRoute<T>(
              opaque: opaque ?? true,
              page: _resolvePage(page, 'to'),
              routeName: routeName,
              gestureWidth: gestureWidth,
              settings: RouteSettings(
                name: routeName,
                arguments: arguments,
              ),
              popGesture: popGesture ?? defaultPopGesture,
              transition: transition ?? defaultTransition,
              curve: curve ?? defaultTransitionCurve,
              fullscreenDialog: fullscreenDialog,
              binding: binding,
              transitionDuration: duration ?? defaultTransitionDuration,
            ),
          );
    }
    

    page作为唯一必传参数,表示要跳转的页面组件,global(id)表示每个id都有一个公有的GlobalKey,可以理解成每个id都有一个单例。因为global(id)返回的是一个GlobalKey<NavigatorState>,所以currentState是NavigatorState,可见,最后也是调用原生的push方法进行路由调度,只不过路由换成了GetPageRoute。

    其他路由方法原理是一样的,都是对于原生的进一步封装。

  • 获取传参

    Get的toNamed方法可以通过arguments属性传递给下一个页面参数,那么怎么获取呢?答案是通过Get.parameters获取,拿到的是一个Map,然后根据自己的key去取对应的值。

    Get.parameters中可以拿到两种方式的值,一种是通过toNamed方法传递的page属性指定的url来传参,另一种是通过toNamed方法的另一个形参arguments指定一个Map参数集合。

    在上面的needRecheck方法中有一句Get.parameters = match.parameters,我们看一下matchRoute方法:

    RouteDecoder matchRoute(String name, {Object? arguments}) {
      final uri = Uri.parse(name);
      // /home/profile/123 => home,profile,123 => /,/home,/home/profile,/home/profile/123
      final split = uri.path.split('/').where((element) => element.isNotEmpty);
      var curPath = '/';
      final cumulativePaths = <String>[
        '/',
      ];
      for (var item in split) {
        if (curPath.endsWith('/')) {
          curPath += '$item';
        } else {
          curPath += '/$item';
        }
        cumulativePaths.add(curPath);
      }
      //_findRoute会从之前GetMaterialApp构造中通过getPages属性添加的路由集合中找出所有符合的路由
      final treeBranch = cumulativePaths
          .map((e) => MapEntry(e, _findRoute(e)))
          .where((element) => element.value != null)
          .map((e) => MapEntry(e.key, e.value!))
          .toList();
    
      final params = Map<String, String>.from(uri.queryParameters);
      if (treeBranch.isNotEmpty) {
        //解析出路由url中携带的参数
        final lastRoute = treeBranch.last;
        final parsedParams = _parseParams(name, lastRoute.value.path);
        if (parsedParams.isNotEmpty) {
          params.addAll(parsedParams);
        }
        //找出每一个路由的arguments的值,把它们赋给MapEntry,此步过后,MapEntry的parameters中会同时持有url中携带的参数和路由中arguments指定的参数
        final mappedTreeBranch = treeBranch
            .map(
              (e) => e.value.copy(
                parameters: {
                  if (e.value.parameters != null) ...e.value.parameters!,
                  ...params,
                },
                name: e.key,
              ),
            )
            .toList();
        return RouteDecoder(
          mappedTreeBranch,
          params,
          arguments,
        );
      }
    
      //route not found
      return RouteDecoder(
        treeBranch.map((e) => e.value).toList(),
        params,
        arguments,
      );
    }
    

    里面有一句final params = Map<String, String>.from(uri.queryParameters),uri.queryParameters就是路由地址中“?”后面的部分,_parseParams方法会把这部分按照url带参标准转成一个参数Map,最终这个params会传给返回的RouteDecoder的parameters属性,RouteDecoder也就是上面的match。

    现在我们知道Get.parameters中已经有路由url本身携带的参数了,再来看toNamed方法传递的参数是怎么保存到Get.parameters的。

    needRecheck方法中往下看,调用了一个addPageParameter方法:

    void addPageParameter(GetPage route) {
      if (route.parameters == null) return;
    
      final parameters = Get.parameters;
      parameters.addEntries(route.parameters!.entries);
      Get.parameters = parameters;
    }
    

    在这个方法中可以看到在这里会把route中的arguments放入Get.parameters中,而route在matchRoute方法中早已把toNamed传递的argumetns保存了。

  • 总结

    现在我们知道,Get框架的路由调用内部也是使用了原生的Navigator,只不过多了一些为了简化使用的封装处理,比如,传递参数的获取是通过Get.parameters统一保存url中的参数和方法传递的参数。

?著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事?!?“怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容