[TOC]
1. Navigator Widget Tree
首先君编,我們可以通過 DevTools 查看一個普通 Flutter App 的 Widget 樹結(jié)構(gòu),與 Navigator 相關(guān)的 Widget 如下圖:
幾個關(guān)鍵的 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)可以創(chuàng)建多個 OverlayEntry
abstract class Route<T> {
RouteSettings _settings;
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
}
// widgets/overlay.dart
class OverlayEntry {
// 用于構(gòu)建 Widget
final WidgetBuilder builder;
// Overlay 是不是不透明的,有什么用后面會提到
bool _opaque;
}
可見椎眯,Navigator 負(fù)責(zé)將頁面棧中所有頁面包含的 OverlayEntry 組織成一個 List挠将,傳遞給 Overlay。
1.2 Overlay
// Overlay 負(fù)責(zé)根據(jù) OverlayEntry 的 opaque 屬性编整,判斷哪些 OverlayEntry 在前臺(onstage)舔稀,
// 哪些在后臺,計算出前臺 OverlayEntry 的數(shù)量掌测,并將其交給 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 狀態(tài),
// Widget 的 build() 仍會執(zhí)行汞斧,但不會渲染
// CupertinoPageRoute 的 maintainState 默認(rèn)為 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 實(shí)質(zhì)上管理的是 RouteEntry竞端,RouteEntry 是對 Route 和 Route 生命周期的封裝。首先庙睡,我們來看看 Flutter 為我們提供了哪些 Route事富。
2.1 Route Family
2.2 Route Lifecycle
跟原生系統(tǒng)一樣,F(xiàn)lutter Route 也有自己的生命周期乘陪。
Navigator 2.0 對 Route 生命周期做了一次大重構(gòu)统台。
3. 應(yīng)用
3.1 Toast
class Toast {
static void show(BuildContext context, String msg, [int lengthInMillis]) async {
// 創(chuàng)建一個 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();
}
}
項(xiàng)目中其他直接使用 Overlay 的場景:
- Bottom Sheet
- iOS Keyboard Header
- Progress Hud
其他典型場景:
- Drag
- Hero
3.2 LocalHistoryRoute
我們項(xiàng)目中大量使用 LocalHistoryRoute 來實(shí)現(xiàn)頁面退出事件攔截:
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
我們項(xiàng)目中會創(chuàng)建一個 PageNavigatorObserver 對象監(jiān)聽路由事件啡邑,然后做一些 PageFlow 相關(guān)的邏輯處理:
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 回調(diào)給的 oldRoute 值有誤贱勃。
首先,我們需要先分析 pushReplacement() 調(diào)用前谣拣,各個 Route 處于生命周期哪個階段:
- A: idle
- B: 因?yàn)?B 此時還在轉(zhuǎn)場募寨,所以狀態(tài)是 poping
然后我們來看看 Navigator pushReplacement() 實(shí)現(xiàn)有什么問題:
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 入棧,初始狀態(tài)為 pushReplace
_history.add(_RouteEntry(newRoute, initialState: _RouteLifecycle.pushReplace));
// 這個方法是 Navigator 的核心森缠,負(fù)責(zé) Route 生命周期調(diào)度
_flushHistoryUpdates();
}
}
_flushHistoryUpdates 調(diào)用前拔鹰,各個 Route 處于生命周期哪個階段:
- A: remove
- B: poping
- C: pushReplace
再來看看 _flushHistoryUpdates 相關(guān)實(shí)現(xiàn):
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);
}
}
}
到此,已經(jīng)找到 didReplace 參數(shù)錯誤的根源贵涵,正確寫法:
observer.didReplace(newRoute: route, oldRoute: previous);
->
observer.didReplace(newRoute: route, oldRoute: previousPresent);
再看看 Google 已經(jīng)在 Flutter Stable 1.20 悄悄做了修復(fù)列肢。