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 with TickerProviderStateMixin {
  // 页面(Route)栈
  List<_RouteEntry> _history = <_RouteEntry>[];
  
  Iterable 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 [],
    );
  }
}

// _RouteEntry 是对 Route 的封装,处理 Add、Push、Pop 等路由事件
class _RouteEntry extends RouteTransitionRecord {
  final Route route;
  void handleAdd() {...}
  void handlePush() {...}
  void handlePop() {...}
}

// 一个页面(Route)可以创建多个 OverlayEntry
abstract class Route {
  RouteSettings _settings;
  List get overlayEntries => const [];
}

// 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 {
  @override
  Widget build(BuildContext context) {
    final List children = [];
    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 {
  @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 extends State with RouteAware {
  void addLocalHistoryEntry(BuildContext context) {
    _localHistoryEntry = LocalHistoryEntry(
      onRemove: onRemove,
    );
    ModalRoute.of(context).addLocalHistoryEntry(_localHistoryEntry);
  }
}

abstract class ModalRoute extends TransitionRoute with LocalHistoryRoute {
}

mixin LocalHistoryRoute on Route {
  void addLocalHistoryEntry(LocalHistoryEntry entry) 
    entry._owner = this;
    _localHistory ??= [];
    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 with TickerProviderStateMixin {
  Future pushReplacement(Route 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 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 previous, @required Route 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 悄悄做了修复。

你可能感兴趣的:(Flutter Navigator 详解)