Flutter Navigator Push流程分析

299 阅读11分钟

引言

本来想很全面的分析一次Navigator1.0 原理,但是发现很难从全貌的角度来讲清楚,最后决定选择从Push流程切入,这样读者既好理解又比较容易通过专一流程尝试了解全貌。

在我上一篇Nav的封装工具类分享中有提过Nav1.0和2.0的区别

  • Navigator 1.0:侧重于移动端的路由系统,采用命令式编程风格,通过push和pop方法管理路由栈。
  • Navigator 2.0:在Flutter 1.22版本后新增,侧重于复杂的桌面端/网页端,采用声明式编程风格,使用Router和RouterInformationParser等类来描述和管理路由树。

路由的简单用法我就不详细去重复说明了,一开始我们先了解一下路由的注册方式,再去深入Push整个流程~

1. 路由注册的方式

1.1 静态/命名路由

首先说下命名路由,就是 需要提前注册的路由,先给每个路由定义一个「字符串名称」,然后通过这个名称来导航到对应的路由,在项目中我们偏于这种方式来注册与跳转路由。

  1. 注册流程
MaterialApp(
      title: "Demo App",
      routes: {"/main":(context)=>DemoPage()}
  )
  1. 跳转方式
Navigator.of(context).pushNamed("/main");

1.2 动态路由

不需要提前注册,可以直接在代码中创建和导航的路由,适用于简单使用或单次导航的场景。

Navigator.push(
  context,
  MaterialPageRoute(builder: (context) => MainScreen()),
);

2. Push流程探索

这里我从pushNamed方法开始讲述,最后会走到跟push一样的流程~

2.1 pushNamed

先把方法放上来,我们看看具体实现。

  Future<T?> pushNamed<T extends Object?>(
    String routeName, {
    Object? arguments,
  }) {
    return push<T?>(_routeNamed<T>(routeName, arguments: arguments)!);
  }
  1. 参数分析

    • routerName:路由的Key,字符串类型,我们可以直接推测出负责从路由Map中获取其builder用的。
    • arguments:路由参数,我们可以通过ModalRoute.of(context)?.settings.arguments;来获取传递的参数
  2. 返回值:Future<T?> 页面在pop的时候,会利用Future来通知并且返回页面结束的参数

    jumpPage() async {
        //页面结束时候会释放await,获取返回值或者通过释放await来获知页面弹出
        dynamic result = await Navigator.push(
              context,
              MaterialPageRoute(builder: (context) => MainScreen()),
            );
    }
    
  3. 方法分析 调用了push方法,但是首先调用了_routerNamed方法,我们先看看routerName这个方法做了什么做了什么

  Route<T?>? _routeNamed<T>(String name, { required Object? arguments, bool allowNull = false }) {
    //当onGenerateRoute为空时,是否直接返回空
    if (allowNull && widget.onGenerateRoute == null) {
      return null;
    }
    //组装RouteSettings,把参数和路径装载进去
    final RouteSettings settings = RouteSettings(
      name: name,
      arguments: arguments,
    );
    //调用onGenerateRoute方法,返回一个Route对象
    Route<T?>? route = widget.onGenerateRoute!(settings) as Route<T?>?;
    //如果找不到路由,则调用onUnknownRoute来返回一个容错路由
    if (route == null && !allowNull) {
      route = widget.onUnknownRoute!(settings) as Route<T?>?;
    }
    return route;
  }

我们阅读注释得知方法两个重点

  1. RouteSetting对象存着arguments参数
  2. 通过onGenerateRoute,生成Route对象,那么我们接着看onGenerateRoute方法的具体实现
  Route<dynamic>? _onGenerateRoute(RouteSettings settings) {
    final String? name = settings.name;
    //1.先判断是否是首页,如果是首页则不浪费性能,直接返回首页的WidgetBuilder
    //2.如果不是首页,则从我们的RouteMap中寻找到WidgetBuilder
    final WidgetBuilder? pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
        ? (BuildContext context) => widget.home!
        : widget.routes![name];
    //找到了页面对应的WidgetBuilder则往下走
    if (pageContentBuilder != null) {
    //3.把RouterSetting和WidgetBuilder再封装成Route对象返回
    //这里的pageRouteBuilder会默认包装成MaterialPageRoute类,影响了进场出场动画等,暂时无需过度关注
      final Route<dynamic> route = widget.pageRouteBuilder!<dynamic>(
        settings,
        pageContentBuilder,
      );
      return route;
    }
    if (widget.onGenerateRoute != null) {
      return widget.onGenerateRoute!(settings);
    }
    return null;
  }

至此routerName方法流程弹栈,我们得知了routerName就是从Map中寻找对应的WidgetBuilder,并且把路径、参数与WidgetBuilder封装到Route对象中,接着我们回去push方法看看。

2.2 push方法流程

class Navigator{
  static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
    //1.调用of方法寻找NavigatorState,这里不展开说,然后调用NavigatorState的push方法
    return Navigator.of(context).push(route);
  }
}
 
 class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin { 
   //2.最后会调用到该方法
   Future<T?> push<T extends Object?>(Route<T> route) {
    //3.封装一个_RouteEntry对象,并且设置状态为push,然后调用_pushEntry方法
    //状态这边我们下面展开说,push状态目前可以理解成目前状态为准备push中
    _pushEntry(_RouteEntry(route, pageBased: false, initialState: _RouteLifecycle.push));
    //4.返回一个Future,这个Future在pop时会调用其complete方法,并且把有可能存在的页面结束参数通过complete方法传递
    //我们知道了为什么push方法能够await和接收参数,再详细的我们后面再说。先聚焦push流程
    return route.popped;
   }
 }

看完上面源码,我们的下一步必然是跟进==pushEntry方法==,但是我们先简单了解一下_RouteEntry的状态有哪些,了解状态是怎么流转的,才能更好的理解后续流程,我们先看看官方状态注释。

// The _RouteLifecycle state machine (only goes down):
//
//                    [creation of a _RouteEntry]
//                                 |
//                                 +
//                                 |\
//                                 | \
//                                 | staging
//                                 | /
//                                 |/
//                    +-+----------+--+-------+
//                   /  |             |       |
//                  /   |             |       |
//                 /    |             |       |
//                /     |             |       |
//               /      |             |       |
//      pushReplace   push*(当前)  add*   replace*
//               \       |            |       |
//                \      |            |      /
//                 +--pushing#      adding  /
//                          \        /     /
//                           \      /     /
//                           idle--+-----+
//                           /  \
//                          /    +------+
//                         /     |      |
//                        /      |  complete*
//                        |      |    /
//                       pop*  remove*
//                        /        \
//                       /       removing#
//                     popping#       |
//                      |             |
//                   [finalizeRoute]  |
//                              \     |
//                              dispose*
//                                 |
//                                 |
//                              disposed
//                                 |
//                                 |
//                  [_RouteEntry garbage collected]
//                          (terminal state)
//

我们可以看出idle是路由状态的分水岭,idel之前的状态,都是路由正在入栈的状态,idel后面的状态,基本是弹栈有关的状态。

那么我们根据这个状态,我们可以看到接下来我们的目的地就是了解路由如何到达idle状态,并且到达idle状态后页面是怎么显示出来的,接着我们继续跟进pushEntry方法,看看状态是怎么流转的。

2.3 状态流转流程

之前的流程只是做前期准备,各种封装,状态的初始化,接下来就是开始状态流程到页面上屏的过程了,刚刚说到我们跟进到pushEntry方法,那么我们继续看看源码。

接下来的流程会较长,我们可以直接把结论说出来,有着结论的引导我们在流程中就不会感受到漫长而迷茫。

结论:

Navigator是通过Overlay来渲染页面的,也就意味着我们的路由要显示到屏幕上的话我们必须得知Route是怎么创建OverlayEntry的,第二是Navigator的组件究竟是怎么回事,不过我们首先知道Route是怎么创建OverlayEntry的吧!

class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {

    //...省略其他不相关代码
    
    //当前的路由栈
    List<_RouteEntry> _history = <_RouteEntry>[];
    
    void _pushEntry(_RouteEntry entry) {
        //1.把路由添加到栈中
        _history.add(entry);
        //2.整理路由栈,开始状态流转
        _flushHistoryUpdates();
    }
    
    //该方法我删掉了与push流程无关的代码,为了专注的跟进流程。
    void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
        //当前页面总数
        int index = _history.length - 1;
        //当前路由的下一个路由
        _RouteEntry? next;
        //当前路由(取的是栈顶路由)
        _RouteEntry? entry = _history[index];
        //当前路由的上一个路由
        _RouteEntry? previous = index > 0 ? _history[index - 1] : null;
        
        while (index >= 0) {
          switch (entry!.currentState) {
            //状态从push开始,所以我们跟进该流程分支
            case _RouteLifecycle.push:
              //1. 调用handlePush方法
              entry.handlePush(
                navigator: this,
                previous: previous?.route,
                previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
                isNewFirst: next == null,
              );
              //2. 从这里到下面的代码我们先忽略,讲完handlePush方法再回来看
              if (entry.currentState == _RouteLifecycle.idle) {
                continue;
              }
              break;
             //...忽略一些代码,不然太长干扰焦点,后面会补充的
          }
        }
  }
}

我们继续跟进handlePush方法

  void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route<dynamic>? previous, required Route<dynamic>? previousPresent }) {
    //获取当前状态,根据刚刚的流程,当前状态为push
    final _RouteLifecycle previousState = currentState;
    route._navigator = navigator;
    //从命名可以直观得知正在装载路由,而该核心是为页面创建OverlayEntry。
    route.install();
    //...忽略代码,先专注install流程,后面会接上
  }

接着跟进router.install方法看看是怎么装载的,在看源码前我们先确定当前Route的实体类为MaterialPageRoute,但是我们在MaterialPageRoute中没找到install方法的实现,那么我就一直往父类去找,我先展示一下继承路径,不然会有点晕

MaterialPageRoute -> PageRoute -> ModalRoute -> TransitionRoute -> OverlayRoute -> Route

在这么长的继承关系中,我们需要关注OverlayRoute和ModalRoute这两个父类。

install最后会调用到OverlayRoute中的install方法,我们跟进一下OverlayRoute这个类。

我们在结论也说到,Navigator是通过Overlay来装载页面的,所以可以确定的是OverlayRoute是负责提供OverlayEntry的一个关键类,我们来看看他的关键构造。

abstract class OverlayRoute<T> extends Route<T> {
  
  OverlayRoute({
    super.settings,
  });

  //创建具体的OverlayEntry迭代器
  @factory
  Iterable<OverlayEntry> createOverlayEntries();

  //保存当前页面所拥有的OverlayEntry
  @override
  List<OverlayEntry> get overlayEntries => _overlayEntries;
  final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];

  //刚刚的route.install会调用到这里。
  @override
  void install() {
    //调用到createOverlayEntries创建路由所需的OverlayEntry,但是我们看出该类不负责具体的创建,接着我们跟进到ModalRoute的createOverlayEntries方法中。
    _overlayEntries.addAll(createOverlayEntries());
    super.install();
  }
}

接着我们具体的跟到ModalRoute中,看看创建了一个怎么样的OverlayEntry

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {

  //蒙层OverlayEntry
  late OverlayEntry _modalBarrier;
  //页面OverlayEntry
  late OverlayEntry _modalScope;

  //创建OverlayEntry
  @override
  Iterable<OverlayEntry> createOverlayEntries() {
    return <OverlayEntry>[
      //创建蒙层Overlay,比如我们dialog中就是灰色底的,具体就是这个蒙层
      _modalBarrier = OverlayEntry(builder: _buildModalBarrier),
      //创建页面的Overlay,留意builder指向了_buildModalScope中
      _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState),
    ];
  }
  
  //该方法就是返回具体的页面Widget了,但是经过层层包装,路径极长,我们还记得注册静态路由的时候的WidgetBuilder吗,ModalScope最终就是调用到widgetBuilder,我看源码的时候都看晕了。
  //输出完痛苦的情绪我们继续,我们省略掉ModalScope的具体过程,因为涉及的类过多路径过长,只能直接说具体实现,ModalScope最后会调用下面的buildPage抽象方法,这个buildPage抽象方法最后会调用我们一开始的widgetBuilder回调返回页面
  Widget _buildModalScope(BuildContext context) {
    return _modalScopeCache ??= Semantics(
      sortKey: const OrdinalSortKey(0.0),
      child: _ModalScope<T>(
        key: _scopeKey,
        route: this,
      ),
    );
  }
  
  //来来来,接着关注这里,_buildModalScope最后会调用这个方法,但是是抽象方法,意味着又要开始往子类找了!!!一起找找这勾八是怎么构建页面的
  Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
}

接下来我们看看buildPage到底是哪个子类在办这事,我们跟进到了MaterialPageRoute中,但是发现MaterialPageRoute没有实现!!干!但是我看到了MaterialPageRoute定义了一个混入类MaterialRouteTransitionMixin,曙光!!我们来看看MaterialRouteTransitionMixin怎么实现buildPage方法的。

mixin MaterialRouteTransitionMixin<T> on PageRoute<T> {
  //看这里
  @override
  Widget buildPage(
    BuildContext context,
    Animation<double> animation,
    Animation<double> secondaryAnimation,
  ) {
    //我操啊,怎么还有个buildContent,往下面看,还需要被混入类来实现!!
    final Widget result = buildContent(context);
  
    return Semantics(
      scopesRoute: true,
      explicitChildNodes: true,
      child: result,
    );
  }
  //MaterialPageRoute实现了该方法!!二话不说继续跟
  @protected
  Widget buildContent(BuildContext context);
}

class MaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> {

  //大家还记得这个吗!!就是WidgetBuilder,存放在了这个类中!!
  final WidgetBuilder builder;

  //实现了混合类的方法!!就是直接调用WidgetBuilder!!!把我们定义的页面返回到OverEntry中!!!!
  @override
  Widget buildContent(BuildContext context) => builder(context);

}

哎,设计得太好了,就是跟起来费脑,可能有一些童鞋看到这里的时候,脑内的都已经爆栈迷路了。但是没关系,我们已经知道了install是如何创建OverlayEntry的,也知道了OverlayEntry中是如何指向到我们的WidgetBuilder的,现在我们一波退栈到install方法那边。

class _RouteEntry extends RouteTransitionRecord {
  //回来了,同学们快醒醒!!
  void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route<dynamic>? previous, required Route<dynamic>? previousPresent }) {
    //获取当前状态,根据刚刚的流程,当前状态为push
    final _RouteLifecycle previousState = currentState;
    route._navigator = navigator;
    //刚刚讲完到这里!!!,我们知道了Route准备好了页面的Widget了!也准备好了Overlay了!
    route.install();
    //我们在install中并没有改变Route的状态,所以会进入该判断
    if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
    //状态变更会pushing
      currentState = _RouteLifecycle.pushing;
      routeFuture.whenCompleteOrCancel(() {
        if (currentState == _RouteLifecycle.pushing) {
         //等一些操作完成后,会回调到这里,意味着路由的上屏已经结束,已经是后话了
          currentState = _RouteLifecycle.idle;
          navigator._flushHistoryUpdates();
        }
      });
    } 
    //...省略一些代码
  }
}

handlerPush在准备好Overlay之后会把状态流转到pushing,此时RouteEntry已经准备好Overlay了,那么刚刚也说到,Navigator是通过Overlay来上屏的。接着Navigator中的State会进行刷新,读取OverlayEntry列表,也就是说接下来我们将要了解Navigator的构造了

class NavigatorState {
     List<_RouteEntry> _history = <_RouteEntry>[];
       @override
  Widget build(BuildContext context) {
  //省略了一些嵌套布局,
    return  Overlay(
              key: _overlayKey,
              clipBehavior: widget.clipBehavior,
              initialEntries: overlay == null ?  _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],//看这里!
        );
  }

}

我们可以看出NavigatorState中返回了Overlay,也就说明了为什么Route不是返回Widget,而是OverlayEntry,因为NavigatorState就是通过Overlay来把页面上屏的,我们再关注initialEntries这个属性,我们传入了_allRouteOverlayEntries,我们再来看看_allRouteOverlayEntries是什么!

  Iterable<OverlayEntry> get _allRouteOverlayEntries {
    return <OverlayEntry>[
      for (final _RouteEntry entry in _history)
        ...entry.route.overlayEntries,
    ];
  }

我已经写得有些不清醒了...但是最终答案来了!!我们Route创建的OverlayEntry,最终被解包到Overlay中,然后开始了渲染流程,最终我们的Route被渲染完成并且状态流转到idle。

至此,push的整个流程完结,我们再来做个简单总结一下,push整个流程是怎么样的?

  1. 组装RouterEntry,创建OverlayEntry和管理状态,并且扔进history中
  2. OverlayEntry最终会调用我们一开始的WidgetBuild来作为其Widget
  3. NavigatorState将Overlay作为组件,通过路由栈中的overlayEntry来显示页面。

以上!就是push方法的整体流程啦!如果觉得对你有帮助的话请不要吝啬的点个赞和收藏文章~谢谢