flutter - 路由

flutter - 路由

前言

flutter 中的页面管理由 Route 和 Navigator 两个核心概念,Route 代表着一个页面的抽象,而 Navigator 则是管理 Route 的 Widget。Navigator可以通过route入栈和出栈来实现页面之间的跳转。

Navigator 作为一个管理路由的控件,继承自 StatefulWidget,通过 NavigatorState 来保存路由栈,负责页面的出入栈,而 MaterialApp 和 WidgetApp 作为必须使用的根 widget 已经在内部集成了 Navigator,所以可以在任意地方都可以通过 Navigator.of(context) 方法通过上下文环境来获取 NavigatorState 实例来操作页面路由,具体的方法也不一一介绍了,这里有几个主要的变量值得介绍一下

1
2
3
4
5
6
7
8
9
10
11
/// Route 列表,先入后出的路由栈
final List<Route<dynamic>> _history = <Route<dynamic>>[];
/// 暂时存放弹出的路由
final Set<Route<dynamic>> _poppedRoutes = Set<Route<dynamic>>();

/// 默认的页面焦点
final FocusScopeNode focusScopeNode = FocusScopeNode();

/// 由 Route 生成的展示页面
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[];

我们可以通过 NavigatorState 的 build 函数来简单的看一下路由器的构成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@override
Widget build(BuildContext context) {
assert(!_debugLocked);
assert(_history.isNotEmpty);
return Listener(
onPointerDown: _handlePointerDown,
onPointerUp: _handlePointerUpOrCancel,
onPointerCancel: _handlePointerUpOrCancel,
child: AbsorbPointer(
absorbing: false, // it's mutated directly by _cancelActivePointers above
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: Overlay(
key: _overlayKey,
initialEntries: _initialOverlayEntries,
),
),
),
);
}

Navigator 会在页面变换时接管触摸事件的拦截取消工作,会生成一个默认的焦点,以及通过 Overlay 最终展示页面层

Route

Route 是一个抽象类,是作为 Navigator 管理路由的单元存在的,这个抽象类定义了一些抽象方法,包括生成用于展示的一个或者多个 OverlayEntry。我们常熟悉的 Route 就是MaterialPageRoute 了,下面是 MaterialPageRoute 的继承图

1
2
3
4
5
6
MaterialPageRoute
\- PageRoute /// A modal route that replaces the entire screen.
\- ModalRoute /// A route that blocks interaction with previous routes.
\- TransitionRoute /// A route with entrance and exit transitions.
\- OverlayRoute /// A route that displays widgets in the [Navigator]'s [Overlay].
\- Route

下面也会以 MaterialPageRoute 为例,详细地分析一下入栈弹出页面时的操作

页面入栈 Navigator.of(context).push(route)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
assert(route != null);
assert(route._navigator == null);
final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
route._navigator = this;
route.install(_currentOverlayEntry);
// 将 route 添加到 NavigatorState 中
_history.add(route);
route.didPush();
route.didChangeNext(null);
if (oldRoute != null) {
oldRoute.didChangeNext(route);
route.didChangePrevious(oldRoute);
}
for (NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute);
assert(() { _debugLocked = false; return true; }());
_afterNavigation();
return route.popped;
}

route.install 方法会准备一些前置操作,比如说在 TransitionRoute 会准备一些 AnimationController 的初始化工作,在 ModalRoute 中会对 AnimationController 进行一定的包装,用于两个页面的转换的动画交互,而在 OverlayRoute 中,则会把 Route 生成的页面即 OverlayEntry 插入到 NavigatorState 的 Overlay 控件中

1
2
3
4
5
6
7
8
@override
void install(OverlayEntry insertionPoint) {
assert(_overlayEntries.isEmpty);
// 一个 route 的视图可能有多层,比如说弹窗的背景阴影
_overlayEntries.addAll(createOverlayEntries());
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
super.install(insertionPoint);
}

route.didPush() 则开始转场动画,主要在 TransitionRoute.didPush 方法中开始,还有比较重要的一点是会为动画设置监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void _handleStatusChanged(AnimationStatus status) {
switch (status) {
case AnimationStatus.completed:
if (overlayEntries.isNotEmpty)
overlayEntries.first.opaque = opaque;
break;
case AnimationStatus.forward:
case AnimationStatus.reverse:
if (overlayEntries.isNotEmpty)
overlayEntries.first.opaque = false;
break;
case AnimationStatus.dismissed:
assert(!overlayEntries.first.opaque);
if (!isCurrent) {
navigator.finalizeRoute(this);
assert(overlayEntries.isEmpty);
}
break;
}
changedInternalState();
}

监听器的目的就是在转场动画播放时将最上面的一层设置为透明,就可以展现完整的动画效果了

1
2
3
4
if (oldRoute != null) {
oldRoute.didChangeNext(route);
route.didChangePrevious(oldRoute);
}

AnimationController 在前面就已经被触发了,
接着的这段代码就是让被覆盖的页面执行一些转场动画,比如说我们熟知的 ModalRoute 中 secondaryAnimation 会通过 TrainHoppingAnimation 这种代理器去关联前面提到的会在 didPush 中运行的 AnimationController,这样就能同步执行前一个页面的退出动画和新页面的进入动画,这些代码可以在 TransitionRoute 中找到,这就是我们通过 PageRouteBuilder 实现的转场动画原理

最后就是去通知一些注册的路由观察者,以及 _afterNavigation 去处理一些后续的事情,比如说取消前一个页面的触摸事件,最后就是返回一个 future

如果理解了路由加载,那么弹出路由就比较容易理解了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@optionalTypeArgs
bool pop<T extends Object>([ T result ]) {
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
final Route<dynamic> route = _history.last;
assert(route._navigator == this);
bool debugPredictedWouldPop;
assert(() { debugPredictedWouldPop = !route.willHandlePopInternally; return true; }());
if (route.didPop(result ?? route.currentResult)) {
assert(debugPredictedWouldPop);
if (_history.length > 1) {
_history.removeLast();
// If route._navigator is null, the route called finalizeRoute from
// didPop, which means the route has already been disposed and doesn't
// need to be added to _poppedRoutes for later disposal.
if (route._navigator != null)
_poppedRoutes.add(route);
_history.last.didPopNext(route);
for (NavigatorObserver observer in widget.observers)
observer.didPop(route, _history.last);
} else {
assert(() { _debugLocked = false; return true; }());
return false;
}
} else {
assert(!debugPredictedWouldPop);
}
assert(() { _debugLocked = false; return true; }());
_afterNavigation();
return true;
}

通过 route.didPop(result ?? route.currentResult) 将值传递回去,TransitionRoute.didPop 中会反向执行动画控制器,OverlayRoute 则会做一些特殊流程的处理工作

根节点是不会被移除的。接着被移除的 route 会被加入到 _poppedRoutes 做统一处理。 _history.last.didPopNext(route) 这里流程和 pop 时一样会执行被返回页面的相关动画。最后就是处理跳转后的一些收尾工作。

原生操作的监听

对于 Android 来说原生系统是由返回键的,那 flutter 是如何将原生事件和 Navigator 关联起来的?

widget 层和 flutter 引擎之间有一层胶水层 WidgetsBinding 负责两层之间的数据交互,在 initInstances 初始化方法中通过下列方法订阅了原生系统的导航事件

1
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);

WidgetBinding._handleNavigationInvocation

1
2
3
4
5
6
7
8
9
Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) {
switch (methodCall.method) {
case 'popRoute':
return handlePopRoute();
case 'pushRoute':
return handlePushRoute(methodCall.arguments);
}
return Future<dynamic>.value();
}

原生会通过 popRoute 和 pushRoute 两个方法控制路由跳转,以 handlePopRoute 为例

1
2
3
4
5
6
7
8
@protected
Future<void> handlePopRoute() async {
for (WidgetsBindingObserver observer in List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPopRoute())
return;
}
SystemNavigator.pop();
}

如果没有对象处理就会交由原生去处理,我们知道 flutter 应用必须要有 WidgetsApp 作为根 widget,而 _WidgetsAppState 实现了 WidgetsBindingObserver 并在 initState 方法中注册自己到 WidgetBinding 中

_WidgetsAppState.didPopRoute

1
2
3
4
5
6
7
8
@override
Future<bool> didPopRoute() async {
assert(mounted);
final NavigatorState navigator = _navigator?.currentState;
if (navigator == null)
return false;
return await navigator.maybePop();
}

最终会关联到 navigator.maybePop 方法上

1
2
3
4
5
6
7
8
9
10
11
12
@optionalTypeArgs
Future<bool> maybePop<T extends Object>([ T result ]) async {
final Route<T> route = _history.last;
assert(route._navigator == this);
final RoutePopDisposition disposition = await route.willPop();
if (disposition != RoutePopDisposition.bubble && mounted) {
if (disposition == RoutePopDisposition.pop)
pop(result);
return true;
}
return false;
}

Route.willPop

1
2
3
Future<RoutePopDisposition> willPop() async {
return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
}

只要 route 不是跟页面就会执行 Navigator.pop 了,而页面添加也是类似的流程

结语

Navigator 当然还有许多的其他相关方法,比如说 pushReplacement 等,但是万变不离其宗,只要了解了 push 和 pop 的流程其他的也就比较容易了解。flutter 的这一套页面路由实现充满了灵活性,让页面间的转场动画实现以及本身的管理变得更加容易,相较于 Android 涉及到 AMS,WMS 等的 Activity 跳转无疑是简单多了,而且也预留了监听接口更是方便的埋点之类额外功能的开发工作。