Flutter 必知必会系列 —— 探索 Route 页面打开过程

前面我们已经介绍了 OverlayRoute等点, 为页面叠加做了完全的准备,这一节我们就解析最常用的一段代码 Navigator.push。和 Navigator 1 相比,Navigator 2 更加声明式,增加了 Page 等API,后面我会专门把官方的 Navigator 2 的设计原则翻译出来,这一节只跟踪 Navigatorpush 过程串联起来前面的关键点。

往期精彩

Flutter 必知必会系列 —— Navigator 的开始 Overlay

Flutter 必知必会系列 —— 无名英雄 _Theatre 舞台剧

Flutter 必知必会系列 —— 全面认识 Route 路由

路由操作的方式

我们的路由操作基本分为三类:打开、关闭、替换。对应到 Navigator 的 API 就是 pushpopreplace
每一类又根据操作的方式分为:直接间接,直接的方式就是直接操作 Route,间接的方式就是通过名字来操作 Route

整体的 API 方法如下:

路由操作API.png

我们最常用的 API 可能就是 pushpoppushpop 是一对相反的操作,所以我们只跟踪 push 过程即可。

添加路由

我们常用的直接添加路由的方式如下:

Navigator.push(context, MaterialPageRoute(builder: (context) {
 return const Text("页面或者对话框");
})); 

Navigator.of(context).push(MaterialPageRoute(builder: (context) {
 return const Text("页面或者对话框");
}));

复制代码

我们直接告诉 Navigator 下一个路由是什么,然后 Navigator 就开始了它的显示流程。

间接添加路由的方式:

Navigator.pushNamed(context, "路由名字");

复制代码

我们告诉 Navigator 路由的名字是什么,Navigator 就会在前期注册的路由表中查名字对应的路由是什么,然后 Navigator 才会开始它的显示流程。

这种间接的方式更加灵活,可以在查名字的时候增加路由拦截。

下面以直接的方式为例,跟踪它的显示流程。

Navigator.push 推入路由

   Navigator.push(context, route)
复制代码

pushNavigator 中的静态方法,这种写法和很多系统的组件相似,其方法内部是:

@optionalTypeArgs
static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
  return Navigator.of(context).push(route); //第一处
}

static NavigatorState of(
  BuildContext context, {
  bool rootNavigator = false,
}) {
  NavigatorState? navigator;
  if (context is StatefulElement && context.state is NavigatorState) {
    navigator = context.state as NavigatorState;
  }
  if (rootNavigator) { //第二处
    navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
  } else {
    navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
  }
  return navigator!;
}
复制代码

我们看第一处的代码,直接调用了 Navigator.ofNavigatorStatefulWidget,它的逻辑都在其 State 中 ———— NavigatorStateof 方法就是返回 NavigatorState

我们再看第二处,注意 rootNavigator 的值,它代表了是否返回最顶层的 NavigatorState,如果是 false,表示向上查找最近的 NavigatorState,如果是 true,表示向上找到最顶层的 NavigatorState

回过头看第一处的代码,rootNavigator 是 false 的,表示只要向上找到最近的 NavigatorState 就可以,我们以下面的例子为例:

navigator 节点.png

如果是在 G 节点调用 Navigator.of(context) 方法,返回的就是 C 节点,如果调用的是 Navigator.of(context, rootNavigator: true) 返回的节点就是 A
同样,在 B 节点向上查找的时候,不管是不是使用 rootNavigator 都会返回 A 节点

上面就是页面打开的第一步,找到管理路由的 NavigatorState,接下来我们看 NavigatorStatepush 操作。

NavigatorState 中的 push

@optionalTypeArgs
Future<T?> push<T extends Object?>(Route<T> route) {
  _pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push)); // 第一处
  return route.popped;
}
复制代码

和大多数 API 一样,Route 也有包装的过程,将我们传入的 MaterialPageRoute 包装成 _RouteEntry,然后执行 _pushEntry 的动作就完事了,所以逻辑集中在 _pushEntry 中。

在介绍后面的内容之前,我们先介绍一下路由的状态。

route声明周期.png

push 和 pop 那一栏主要是开发者调用了系统的 API,状态和方法名一样,具体的状态含义如下:

状态名 含义
add onGenerateInitialRoute 或者 pages 生成的 Route ,之后会调用 install
adding 等待顶层路由的结果
push 通过 push 生成的路由,之后会调用 install
pushReplace 通过 pushReplace 生成的路由,之后会调用 install
pushing 等待顶层路由的结果
replace 通过 replace 生成的路由,之后会调用 install
idle 路由已经稳定了,显示在页面上
pop 路由要关闭,下一步调用 didPop
remove 删除路由,下一步调用 didReplace/didRemove
popping 等待 finalizeRoute 的调用
removing 等待动画的完成,会移除 overlay 中的页面内容
dispose 马上释放路由
disposed 路由已经释放掉了

我们再看上面的第一处代码,

 _pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push)); // 第一处
复制代码

因为我们调用了 push 的方法,所以构造的 _RouteEntry 的状态是 push。下面我们看具体的 _pushEntry 逻辑。

 void _pushEntry(_RouteEntry entry) {
    _history.add(entry); //第一处
    _flushHistoryUpdates(); //第二处
    _afterNavigation(entry.route); //第三处
  }
 
复制代码

我们先看第一处的代码,是成员变量 _history 添加了路由包装类,这里我们简单介绍一下 _historyNavigator 2.0 的设计原则就是更新 _history 来实现声明式的效果,_history 里面就是存放的已经打开的路由包装类,_history 数组最后一个元素就是当前栈顶的路由或者要添加的路由

我们在看第二处的 _flushHistoryUpdates,它的作用就是刷新栈顶数据,我们稍后看。

第三处的 _afterNavigation,就是将一些手势事件取消掉。

所以,从名字可以看出来,逻辑集中在第二处的方法里。

小结一下:

posh 过程.png

刷新历史路有栈

下面我们集中精力看 _flushHistoryUpdates

push 代码逻辑.png

上面的代码基本分为三部分:变量初始化、根据路由状态调用不同的逻辑、显示路由内容

变量初始化

push 代码逻辑 2.png

index :表示当前遍历到的路由索引,第一个遍历到的就是栈顶的路由(我们刚添加的)索引
entry:表示当前遍历到的路由,第一个遍历到的就是栈顶的路由
previous:表示 entry 的前一个路由。

我们举个例子:

页面route.png

这一 part 主要是变量的赋值,记住它的含义就可以,下面我们看具体的处理逻辑。

根据路由状态调用不同的逻辑

我们调用 Navigator.push 的时候,状态就是 _RouteLifecycle.push ,所以就走到了 entry.handlePush 中。我们在看其中的逻辑。
这就是包装类的作用,原始的路由类中并没有 handlePush 的方法,而包装类起到了类增强的效果,和之前的 OverlayEntry 很像。

push 代码逻辑 3.png

我们先介绍方法的入参:

参数名 含义
navigator 对象 NavigatorState,承载路由的壳子组件
previous 前遍历到的路由的前一个
previousPresent 当前遍历到的路由的前一个,和 previous 相比,previousPresent 路由的状态一定是存在的,而 previous 可能是 remove 的
isNewFirst 是不是要插入到栈顶的路由

参考上面的图:

C 路由是要 push 进来的,AB 是已经在页面中的。如果 B 的状态是在** add 和 remove** 之间的,比如是 idle 的,那么 CpreviousPresent 就是 B,否则就是 A

知道了这个我们看其中的逻辑:

  void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route<dynamic>? previous, required Route<dynamic>? previousPresent }) {
  
   final _RouteLifecycle previousState = currentState;
   
   route._navigator = navigator;
   route.install(); //第一处
   
   if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
     final TickerFuture routeFuture = route.didPush(); //第二处
     currentState = _RouteLifecycle.pushing;
     routeFuture.whenCompleteOrCancel(() {
       if (currentState == _RouteLifecycle.pushing) {
         currentState = _RouteLifecycle.idle;
         navigator._flushHistoryUpdates();
       }
     });
   } else {
     route.didReplace(previous);
     currentState = _RouteLifecycle.idle;
   }
   if (isNewFirst) {
     route.didChangeNext(null);
   }
  ///... 省略代码
 }
复制代码

我们看第一处的代码,这个是不是很熟悉呀,就是路由的初始化。还记得这个初始化是啥吗?可以想看前面的线性初始化
我们的 RouteMaterialPageRoute,就看其中的代码。

install.png

做好了显示的准备工作,我们知道包装类其实不会做具体的逻辑的,真正执行 push 的逻辑还是在 Route 中,所以就是第二处的代码,调用了 RoutedidPush。同样的道理,didPush 也是线性的继承的,和初始化相比,didPush 简单的多,我们下面来看:


Route:
TickerFuture didPush() {
   return TickerFuture.complete()..then<void>((void _) {
     if (navigator?.widget.requestFocus == true) {
       navigator!.focusScopeNode.requestFocus();
     }
   });
 }

 TransitionRoute:
 @override
 TickerFuture didPush() {
   super.didPush();
   return _controller!.forward();
 }
 
 ModalRoute:
 @override
 TickerFuture didPush() {
   if (_scopeKey.currentState != null && navigator!.widget.requestFocus) {
     navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode);
   }
   return super.didPush();
 }

复制代码

我们看到 didPush 就做了两件事:焦点控制和动画驱动。这里注意一点这个动画就是初始化时 ModalRoute 中的 buildTransitions 的动画进度,只要 _controller 的动画进度变化了,buildTransitions 就会被调用。

反过来,我们在看上面的第二处,执行完 didPush 之后,就将 Route 的状态设置为了 _RouteLifecycle.pushing

这就是包装类的 handlePush 的指定流程:Route 的初始化、驱动动画、路由的状态设置为 pushing。

显示路由

push 代码逻辑 4.png

显示路由代码的很简单,就是将初初始化的 OverlayEntry 添加到 Overlay 中。

添加的方式和我们之前介绍过的 add 不同Flutter 必知必会系列 —— Navigator 的开始 Overlay,而是重新组织内容。_allRouteOverlayEntries 这个变量保存的是所有历史路由的 OverlayEntry,这里注意一点,每一个路由中含有两个 OverlayEntry

  void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry? below, OverlayEntry? above }) {
   final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry> ? newEntries : newEntries.toList(growable: false);
   if (newEntriesList.isEmpty)
     return;
   if (listEquals(_entries, newEntriesList))
     return;
   final LinkedHashSet<OverlayEntry> old = LinkedHashSet<OverlayEntry>.from(_entries);
   for (final OverlayEntry entry in newEntriesList) {
     entry._overlay ??= this;
   }
   setState(() {
     _entries.clear();
     _entries.addAll(newEntriesList); 
     old.removeAll(newEntriesList);
     _entries.insertAll(_insertionIndex(below, above), old);//第一处
   });
 }
复制代码

重点看第一处,就是调用了我们之前讲过的 insert 流程。走到这里大家就知道了把,Navigator 的路由管理就是把路由的 OverlayEntry 添加了 NavigatorOverlay 中。动画是怎么做的呢?就是在我们的页面上增加了一个动画组件来响应动画驱动器而已,一个路由的显示层级如下:

页面层级.png

总结

Navigator 的页面显示就是 Overlay 显示 OverlayEntry,我们自己也可以开发一个简约版的叠加,不同的是 Navigator 为页面的显示增加了过度动画,焦点控制等。除此之外,不知道大家知道为啥路由中要有两个 OverlayEntry 不~~~

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享