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