Flutter 必知必会系列 —— 探索 Route 页面打开过程
前面我们已经介绍了 Overlay 、Route等点, 为页面叠加做了完全的准备,这一节我们就解析最常用的一段代码 Navigator.push。和 Navigator 1 相比,Navigator 2 更加声明式,增加了 Page 等API,后面我会专门把官方的 Navigator 2 的设计原则翻译出来,这一节只跟踪 Navigator 的 push 过程串联起来前面的关键点。
往期精彩
路由操作的方式
我们的路由操作基本分为三类:打开、关闭、替换。对应到 Navigator 的 API 就是 push、pop 和 replace。
每一类又根据操作的方式分为:直接 和 间接,直接的方式就是直接操作 Route,间接的方式就是通过名字来操作 Route。
整体的 API 方法如下:

我们最常用的 API 可能就是 push 和 pop。 push 和 pop 是一对相反的操作,所以我们只跟踪 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)
push 是 Navigator 中的静态方法,这种写法和很多系统的组件相似,其方法内部是:
@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.of,Navigator 是 StatefulWidget,它的逻辑都在其 State 中 ———— NavigatorState,of 方法就是返回 NavigatorState。
我们再看第二处,注意 rootNavigator 的值,它代表了是否返回最顶层的 NavigatorState,如果是 false,表示向上查找最近的 NavigatorState,如果是 true,表示向上找到最顶层的 NavigatorState。
回过头看第一处的代码,rootNavigator 是 false 的,表示只要向上找到最近的 NavigatorState 就可以,我们以下面的例子为例:

如果是在 G 节点调用 Navigator.of(context) 方法,返回的就是 C 节点,如果调用的是 Navigator.of(context, rootNavigator: true) 返回的节点就是 A。
同样,在 B 节点向上查找的时候,不管是不是使用 rootNavigator 都会返回 A 节点。
上面就是页面打开的第一步,找到管理路由的 NavigatorState,接下来我们看 NavigatorState 的 push 操作。
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 中。
在介绍后面的内容之前,我们先介绍一下路由的状态。

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 添加了路由包装类,这里我们简单介绍一下 _history,Navigator 2.0 的设计原则就是更新 _history 来实现声明式的效果,_history 里面就是存放的已经打开的路由包装类,_history 数组最后一个元素就是当前栈顶的路由或者要添加的路由。
我们在看第二处的 _flushHistoryUpdates,它的作用就是刷新栈顶数据,我们稍后看。
第三处的 _afterNavigation,就是将一些手势事件取消掉。
所以,从名字可以看出来,逻辑集中在第二处的方法里。
小结一下:

刷新历史路有栈
下面我们集中精力看 _flushHistoryUpdates。

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

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

这一 part 主要是变量的赋值,记住它的含义就可以,下面我们看具体的处理逻辑。
根据路由状态调用不同的逻辑
我们调用 Navigator.push 的时候,状态就是 _RouteLifecycle.push ,所以就走到了 entry.handlePush 中。我们在看其中的逻辑。
这就是包装类的作用,原始的路由类中并没有 handlePush 的方法,而包装类起到了类增强的效果,和之前的 OverlayEntry 很像。

我们先介绍方法的入参:
| 参数名 | 含义 |
|---|---|
| navigator | 对象 NavigatorState,承载路由的壳子组件 |
| previous | 前遍历到的路由的前一个 |
| previousPresent | 当前遍历到的路由的前一个,和 previous 相比,previousPresent 路由的状态一定是存在的,而 previous 可能是 remove 的 |
| isNewFirst | 是不是要插入到栈顶的路由 |
参考上面的图:
C 路由是要 push 进来的,A 和 B 是已经在页面中的。如果 B 的状态是在** add 和 remove** 之间的,比如是 idle 的,那么 C 的 previousPresent 就是 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);
}
///... 省略代码
}

做好了显示的准备工作,我们知道包装类其实不会做具体的逻辑的,真正执行 push 的逻辑还是在 Route 中,所以就是第二处的代码,调用了 Route 的 didPush。同样的道理,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。
显示路由

显示路由代码的很简单,就是将初初始化的 OverlayEntry 添加到 Overlay 中。
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 添加了 Navigator 的 Overlay 中。动画是怎么做的呢?就是在我们的页面上增加了一个动画组件来响应动画驱动器而已,一个路由的显示层级如下:

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