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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342