[TOC]
1. Navigator Widget Tree
首先,我们可以通过 DevTools 查看一个普通 Flutter App 的 Widget 树结构,与 Navigator 相关的 Widget 如下图:
几个关键的 Widget 分别是 Navigator、Overlay 和 Threatre,接下来我们通过阅读源码来看看 Flutter 是如何通过这几个 Widget 来组织页面(Route)的。
1.1 Navigator
class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
// 页面(Route)栈
List<_RouteEntry> _history = <_RouteEntry>[];
Iterable<OverlayEntry> get _allRouteOverlayEntries sync* {
for (final _RouteEntry entry in _history)
yield* entry.route.overlayEntries;
}
@override
Widget build(BuildContext context) {
return Overlay(
key: _overlayKey,
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
);
}
}
// _RouteEntry 是对 Route 的封装,处理 Add、Push、Pop 等路由事件
class _RouteEntry extends RouteTransitionRecord {
final Route<dynamic> route;
void handleAdd() {...}
void handlePush() {...}
void handlePop() {...}
}
// 一个页面(Route)可以创建多个 OverlayEntry
abstract class Route<T> {
RouteSettings _settings;
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
}
// widgets/overlay.dart
class OverlayEntry {
// 用于构建 Widget
final WidgetBuilder builder;
// Overlay 是不是不透明的,有什么用后面会提到
bool _opaque;
}
可见,Navigator 负责将页面栈中所有页面包含的 OverlayEntry 组织成一个 List,传递给 Overlay。
1.2 Overlay
// Overlay 负责根据 OverlayEntry 的 opaque 属性,判断哪些 OverlayEntry 在前台(onstage),
// 哪些在后台,计算出前台 OverlayEntry 的数量,并将其交给 Theatre
class OverlayState extends State<Overlay> {
@override
Widget build(BuildContext context) {
final List<Widget> children = <Widget>[];
bool onstage = true;
int onstageCount = 0;
for (int i = _entries.length - 1; i >= 0; i -= 1) {
final OverlayEntry entry = _entries[I];
if (onstage) {
onstageCount += 1;
children.add(_OverlayEntryWidget(
key: entry._key,
entry: entry,
));
if (entry.opaque)
onstage = false;
} else if (entry.maintainState) {
// maintainState 为 true 时,即使 Route 处于 Offstage 状态,
// Widget 的 build() 仍会执行,但不会渲染
// CupertinoPageRoute 的 maintainState 默认为 true
children.add(_OverlayEntryWidget(
key: entry._key,
entry: entry,
tickerEnabled: false,
));
}
}
return _Theatre(
skipCount: children.length - onstageCount,
children: children.reversed.toList(growable: false),
);
}
}
1.3 Theatre
class _Theatre extends MultiChildRenderObjectWidget {
@override
_RenderTheatre createRenderObject(BuildContext context) {
return _RenderTheatre(
skipCount: skipCount,
textDirection: Directionality.of(context),
);
}
}
class _RenderTheatre extends RenderBox with ContainerRenderObjectMixin<RenderBox, StackParentData> {
@override
void paint(PaintingContext context, Offset offset) {
RenderBox child = _firstOnstageChild;
while (child != null) {
final StackParentData childParentData = child.parentData as StackParentData;
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}
RenderBox get _firstOnstageChild {
RenderBox child = super.firstChild;
for (int toSkip = skipCount; toSkip > 0; toSkip--) {
final StackParentData childParentData = child.parentData as StackParentData;
child = childParentData.nextSibling;
assert(child != null);
}
return child;
}
}
_Theatre 会跳过 offstage Overlay,只绘制 onstage Overlay。
2. Route
上一章我们看到,Navigator 实质上管理的是 RouteEntry,RouteEntry 是对 Route 和 Route 生命周期的封装。首先,我们来看看 Flutter 为我们提供了哪些 Route。
2.1 Route Family
2.2 Route Lifecycle
跟原生系统一样,Flutter Route 也有自己的生命周期。
Navigator 2.0 对 Route 生命周期做了一次大重构。
3. 应用
3.1 Toast
class Toast {
static void show(BuildContext context, String msg, [int lengthInMillis]) async {
// 创建一个 OverlayEntry, opaque = false
final overlayEntry = OverlayEntry(
builder: (context) => _ToastWidget(),
opaque: false,
);
// 直接往 OverlayState 里插入 OverlayEntry
final overlayState = Overlay.of(context);
overlayState.insert(overlayEntry);
// 展示一段时间后再从 OverlayState 中移除自己
await Future.delayed(Duration(milliseconds: lengthInMillis ?? Toast.lengthShort));
overlayEntry?.remove();
}
}
项目中其他直接使用 Overlay 的场景:
- Bottom Sheet
- iOS Keyboard Header
- Progress Hud
其他典型场景:
- Drag
- Hero
3.2 LocalHistoryRoute
我们项目中大量使用 LocalHistoryRoute 来实现页面退出事件拦截:
abstract class BasePageState<T extends StatefulWidget> extends State<T> with RouteAware {
void addLocalHistoryEntry(BuildContext context) {
_localHistoryEntry = LocalHistoryEntry(
onRemove: onRemove,
);
ModalRoute.of(context).addLocalHistoryEntry(_localHistoryEntry);
}
}
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
}
mixin LocalHistoryRoute<T> on Route<T> {
void addLocalHistoryEntry(LocalHistoryEntry entry)
entry._owner = this;
_localHistory ??= <LocalHistoryEntry>[];
final bool wasEmpty = _localHistory.isEmpty;
_localHistory.add(entry);
if (wasEmpty)
changedInternalState();
}
@override
bool didPop(T result) {
if (_localHistory != null && _localHistory.isNotEmpty) {
final LocalHistoryEntry entry = _localHistory.removeLast();
entry._owner = null;
entry._notifyRemoved();
if (_localHistory.isEmpty)
changedInternalState();
return false;
}
return super.didPop(result);
}
}
3.3 Push Replacement Bug
我们项目中会创建一个 PageNavigatorObserver 对象监听路由事件,然后做一些 PageFlow 相关的逻辑处理:
class PageNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route route, Route previousRoute) {...}
@override
void didPop(Route route, Route previousRoute) {...}
@override
void didReplace({Route newRoute, Route oldRoute}) {...}
}
考虑以下场景:
NavigatorObserver disReplace 回调给的 oldRoute 值有误。
首先,我们需要先分析 pushReplacement() 调用前,各个 Route 处于生命周期哪个阶段:
- A: idle
- B: 因为 B 此时还在转场,所以状态是 poping
然后我们来看看 Navigator pushReplacement() 实现有什么问题:
class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
Future<T> pushReplacement<T extends Object, TO extends Object>(Route<T> newRoute, { TO result }) {
// Present: add, adding, push, pushReplace, pushing, replace, idle, pop, remove
// 先将 A 置为 remove
_history.lastWhere(_RouteEntry.isPresentPredicate).complete(result, isReplaced: true);
// 将 C 入栈,初始状态为 pushReplace
_history.add(_RouteEntry(newRoute, initialState: _RouteLifecycle.pushReplace));
// 这个方法是 Navigator 的核心,负责 Route 生命周期调度
_flushHistoryUpdates();
}
}
_flushHistoryUpdates 调用前,各个 Route 处于生命周期哪个阶段:
- A: remove
- B: poping
- C: pushReplace
再来看看 _flushHistoryUpdates 相关实现:
class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
// 从后往前遍历 _history
int index = _history.length - 1;
_RouteEntry previous = index > 0 ? _history[index - 1] : null;
while (index >= 0) {
switch (entry.currentState) {
...
case _RouteLifecycle.push:
case _RouteLifecycle.pushReplace:
case _RouteLifecycle.replace:
entry.handlePush(
navigator: this,
previous: previous?.route, // B
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route, // A
isNewFirst: next == null,
);
break;
}
index -= 1;
previous = index > 0 ? _history[index - 1] : null;
}
}
}
// previous: B, previousPresent: A
class _RouteEntry extends RouteTransitionRecord {
void handlePush({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) {
...
if (previousState == _RouteLifecycle.replace || previousState == _RouteLifecycle.pushReplace) {
for (final NavigatorObserver observer in navigator.widget.observers)
observer.didReplace(newRoute: route, oldRoute: previous); // What?
} else {
for (final NavigatorObserver observer in navigator.widget.observers)
observer.didPush(route, previousPresent);
}
}
}
到此,已经找到 didReplace 参数错误的根源,正确写法:
observer.didReplace(newRoute: route, oldRoute: previous);
->
observer.didReplace(newRoute: route, oldRoute: previousPresent);
再看看 Google 已经在 Flutter Stable 1.20 悄悄做了修复。