Flutter Navigator 詳解

[TOC]

1. Navigator Widget Tree

首先,我們可以通過 DevTools 查看一個普通 Flutter App 的 Widget 樹結構,與 Navigator 相關的 Widget 如下圖:

Navigator Widget Tree

幾個關鍵的 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

Route Family

2.2 Route Lifecycle

跟原生系統一樣,Flutter Route 也有自己的生命周期。
Navigator 2.0 對 Route 生命周期做了一次大重構。

Route Lifecycle

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}) {...}
}

考慮以下場景:

Pop And Push Replacement

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 悄悄做了修復

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。