likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 11

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

往期精彩

路由操作的方式

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

整体的 API 方法如下:

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

我们最常用的 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 就可以,我们以下面的例子为例:

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

如果是在 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 中。

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

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

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

状态名含义
addonGenerateInitialRoute 或者 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,就是将一些手势事件取消掉。

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

小结一下:

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

刷新历史路有栈

下面我们集中精力看 _flushHistoryUpdates

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

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

变量初始化

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

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

我们举个例子:

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

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

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

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

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

我们先介绍方法的入参:

参数名含义
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);
   }
  ///... 省略代码
 }

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

做好了显示的准备工作,我们知道包装类其实不会做具体的逻辑的,真正执行 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。

显示路由

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

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

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

总结

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