flutter 路由機制

無若葉發表於2021-07-10

參考程式碼版本,1.18

整個 flutter 應用的執行都只是基於原生應用中的一個 view,比如 android 中的 FlutterView,flutter 中的頁面切換依賴於它的路由機制,也就是以 Navigator 為中心的一套路由功能,使得它能夠完成與原生類似且能夠自定義的頁面切換效果。

下面將介紹 flutter 中的路由實現原理,包括初始化時的頁面載入、切換頁面的底層機制等。

實現基礎

flutter 應用的執行需要依賴 MaterialApp/CupertinoApp 這兩個 Widget,他們分別對應著 android/ios 的設計風格,同時也為應用的執行提供了一些基本的設施,比如與路由相關的主頁面、路由表等,再比如跟整體頁面展示相關的 theme、locale 等。

其中與路由相關的幾項配置有 home、routes、initialRoute、onGenerateRoute、onUnknownRoute,它們分別對應著主頁面 widget、路由表(根據路由找到對應 widget)、首次載入時的路由、路由生成器、未知路由代理(比如常見的 404 頁面)。

MaterialApp/CupertinoApp 的子結點都是 WidgetsApp,只不過他們給 WidgetsApp 傳入了不同的引數,從而使得兩種 Widget 的介面風格不一致。Navigator 就是在 WidgetsApp 中建立的,

Widget build(BuildContext context) {
  Widget navigator;
    if (_navigator != null) {
    navigator = Navigator(
      key: _navigator,
      // If window.defaultRouteName isn't '/', we should assume it was set
      // intentionally via `setInitialRoute`, and should override whatever
      // is in [widget.initialRoute].
      initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
          ? WidgetsBinding.instance.window.defaultRouteName
          : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
      onGenerateRoute: _onGenerateRoute,
      onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
        ? Navigator.defaultGenerateInitialRoutes
        : (NavigatorState navigator, String initialRouteName) {
          return widget.onGenerateInitialRoutes(initialRouteName);
        },
      onUnknownRoute: _onUnknownRoute,
      observers: widget.navigatorObservers,
    );
  }
  ...
}
複製程式碼

在 WidgetsApp 的 build 中第一個建立的就是 Navigator,主要看一下它的引數,首先,_navigator 是一個 GlobalKey,使得 WidgetsApp 可以通過 key 呼叫 Navigator 的函式進行路由切換,也就是在 WidgetsBinding 中處理 native 的路由切換資訊的時候,最終是由 WidgetsApp 完成的。另外這裡的 _navigator 應該只在 WidgetsApp 中有使用,其他地方需要使用一般是直接呼叫 Navigator.of 獲取,這個函式會沿著 element 樹向上查詢到 NavigatorState,所以在應用中切換路由是需要被 Navigator 包裹的,不過由於 WidgetsApp 中都有生成 Navigator,開發中也不必考慮這些。

另外,就是關於底層獲取上層 NavigatorElement 例項的方式,在 Element 樹中有兩種方式可以從底層獲取到上層的例項,一種方式是使用 InheritedWidget,另一種就是直接沿著樹向上查詢(ancestorXXXOfExactType 系列),兩種方式的原理基本是一致的,只不過 InheritedWidget 在建立樹的過程中會一層層向下傳遞,而後者是使用的時候才向上查詢,所以從這個角度來說使用 InheritedWidget 會高效些,但是 InheritedWidget 的優勢不止如此,它是能夠在資料發生改變的時候通知所有依賴它的結點進行更新,這也是 ancestorXXXOfExactType 系列所沒有的。

然後 initialRoute 規定了初始化時候的頁面,由 WidgetsBinding.instance.window.defaultRouteName 和 widget.initialRoute 來決定,不過前者優先順序更高,因為這個是 native 中指定的,以 android 為例,在啟動 FlutterActivity 的時候可以傳入 route 欄位指定初始化頁面。

onGenerateRoute 和 onUnknownRoute 是獲取 route 的策略,當 onGenerateRoute 沒有命中時會呼叫 onUnknownRoute 給定一個預設的頁面,onGenerateInitialRoutes 用於生產啟動應用時的路由列表,它有一個預設實現 defaultGenerateInitialRoutes,會根據傳遞的 initialRouteName 選擇不同的 Route,如果傳入的 initialRouteName 並不是預設的主頁面路由 Navigator.defaultRouteName,flutter 並不會將 initRoute 作為主頁面,而是將預設路由入棧了之後再入棧 initRoute 對應的頁面,所以如果在這之後再呼叫 popRoute,是會返回到主頁面的

observers 是路由切換的監聽列表,可以由外部傳入,在路由切換的時候做些操作,比如 HeroController 就是一個監聽者。

Navigator 是一個 StatefulWidget,在 NavigatorState 的 initState 中完成了將 initRoute 轉換成 Route 的過程,並呼叫 push 將其入棧,生成 OverlayEntry,這個會繼續傳遞給下層負責顯示頁面的 Overlay 負責展示。

在 push 的過程中,route 會被轉換成 OverlayEntry 列表存放,每一個 OverlayEntry 中儲存一個 WidgetBuilder,從某種角度來說,OverlayEntry 可以被認為是一個頁面。所有的頁面的協調、展示是通過 Overlay 完成的,Overlay 是一個類似於 Stack 的結構,它可以展示多個子結點。在它的 initState 中,

void initState() {
  super.initState();
  insertAll(widget.initialEntries);
}
複製程式碼

會將 initialEntries 都存到 _entries 中。

Overlay 作為一個能夠根據路由確定展示頁面的控制元件,它的實現其實比較簡單:

Widget build(BuildContext context) {
  // These lists are filled backwards. For the offstage children that
  // does not matter since they aren't rendered, but for the onstage
  // children we reverse the list below before adding it to the tree.
  final List<Widget> onstageChildren = <Widget>[];
  final List<Widget> offstageChildren = <Widget>[];
  bool onstage = true;
  for (int i = _entries.length - 1; i >= 0; i -= 1) {
    final OverlayEntry entry = _entries[i];
    if (onstage) {
      onstageChildren.add(_OverlayEntry(entry));
      if (entry.opaque)
        onstage = false;
    } else if (entry.maintainState) {
      offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
    }
  }
  return _Theatre(
    onstage: Stack(
      fit: StackFit.expand,
      children: onstageChildren.reversed.toList(growable: false),
    ),
    offstage: offstageChildren,
  );
}
複製程式碼

build 函式中,將所有的 OverlayEntry 分成了可見與不可見兩部分,每一個 OverlayEntry 生成一個 _OverlayEntry,這是一個 StatefulWidget,它的作用主要是負責控制當前頁重繪,都被封裝成 然後再用 _Theatre 展示就完了,在 _Theatre 中,可見/不可見的子結點都會轉成 Element,但是在繪製的時候,_Theatre 對應的 _RenderTheatre 只會把可見的子結點繪製出來。

判斷某一個 OverlayEntry 是否能夠完全遮擋上一個 OverlayEntry 是通過它的 opaque 變數判斷的,而 opaque 又是由 Route 給出的,在頁面動畫執行時,這個值會被設定成 false,然後在頁面切換動畫執行完了之後就會把 Route 的 opaque 引數賦值給它的 OverlayEntry,一般情況下,視窗對應的 Route 為 false,頁面對應的 Route 為 true。

所以說在頁面切換之後,上一個頁面始終都是存在於 element 樹中的,只不過在 RenderObject 中沒有將其繪製出來,這一點在 Flutter Outline 工具裡面也能夠體現。從這個角度也可以理解為,在 flutter 中頁面越多,需要處理的步驟就越多,雖然不需要繪製底部的頁面,但是整個樹的基本遍歷還是會有的,這部分也算是開銷。

_routeNamed

flutter 中進行頁面管理主要的依賴路由管理系統,它的入口就是 Navigator,它所管理的東西,本質上就是承載著使用者頁面的 Route,但是在 Navigator 中有很多函式是 XXXName 系列的,它們傳的不是 Route,而是 RouteName,據個人理解,這個主要是方便開發引入的,我們可以在 MaterialApp/CupertinoApp 中直接傳入路由表,每一個名字對應一個 WidgetBuilder,然後結合 pageRouteBuilder(這個可以自定義,不過 MaterialApp/CupertinoApp 都有預設實現,能夠將 WidgetBuilder 轉成 Route),便可以實現從 RouteName 到 Route 的轉換。

Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) {
  if (allowNull && widget.onGenerateRoute == null)
    return null;
  final RouteSettings settings = RouteSettings(
    name: name,
    arguments: arguments,
  );
  Route<T> route = widget.onGenerateRoute(settings) as Route<T>;
  if (route == null && !allowNull) {
    route = widget.onUnknownRoute(settings) as Route<T>;
  }
  return route;
}
複製程式碼

這個過程分三步,生成 RouteSettings,呼叫 onGenerateRoute 從路由表中拿到對應的路由,如果無命中,就呼叫 onUnknownRoute 給一個類似於 404 頁面的東西。

onGenerateRoute 和 onUnknownRoute 在構建 Navigator 時傳入,在 WidgetsApp 中實現,

Route<dynamic> _onGenerateRoute(RouteSettings settings) {
  final String name = settings.name;
  final WidgetBuilder pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
      ? (BuildContext context) => widget.home
      : widget.routes[name];
  if (pageContentBuilder != null) {
    final Route<dynamic> route = widget.pageRouteBuilder<dynamic>(
      settings,
      pageContentBuilder,
    );
    return route;
  }
  if (widget.onGenerateRoute != null)
    return widget.onGenerateRoute(settings);
  return null;
}
複製程式碼

如果是預設的路由會直接使用給定的 home 頁面(如果有),否則就直接到路由表查,所以本質上這裡的 home 頁面更多的是一種象徵,身份的象徵,沒有也無所謂。另外路由表主要的產出是 WidgetBuilder,它需要經過一次包裝,成為 Route 才是成品,或者如果不想使用路由表這種,也可以直接實現 onGenerateRoute 函式,根據 RouteSetting 直接生成 Route,這個就不僅僅是返回 WidgetBuilder 這麼簡單了,需要自己包裝。

onUnknownRoute 主要用於兜底,提供一個類似於 404 的頁面,它也是需要直接返回 Route。

_flushHistoryUpdates

不知道從哪一個版本開始,flutter 的路由管理引入了狀態,與之前每一個 push、pop 都單獨實現不同,所有的路由切換操作都是用狀態表示,同時所有的 route 都被封裝成 _RouteEntry,它內部有著關於 Route 操作的實現,但都被劃分為比較小的單元,且都依靠狀態來執行。

狀態是一個具有遞進關係的列舉,每一個 _RouteEntry 都有一個變數存放當前的狀態,在 _flushHistoryUpdates 中會遍歷所有的 _RouteEntry 然後根據它們當前的狀態進行處理,同時處理完成之後會切換它們的狀態,再進行其他處理,這樣的好處很明顯,所有的路由都放在一起處理之後,整個流程會變得更加清晰,且能夠很大程度上進行程式碼複用,比如 push 和 pushReplacement 兩種操作,這在之前是需要在兩個方法中單獨實現的,而現在他們則可以放在一起單獨處理,不同的只有後者比前者會多一個 remove 的操作。

關於 _flushHistoryUpdates 的處理步驟:

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  assert(_debugLocked && !_debugUpdatingPage);
  // Clean up the list, sending updates to the routes that changed. Notably,
  // we don't send the didChangePrevious/didChangeNext updates to those that
  // did not change at this point, because we're not yet sure exactly what the
  // routes will be at the end of the day (some might get disposed).
  int index = _history.length - 1;
  _RouteEntry next;
  _RouteEntry entry = _history[index];
  _RouteEntry previous = index > 0 ? _history[index - 1] : null;
  bool canRemoveOrAdd = false; // Whether there is a fully opaque route on top to silently remove or add route underneath.
  Route<dynamic> poppedRoute; // The route that should trigger didPopNext on the top active route.
  bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.
  final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
  while (index >= 0) {
    switch (entry.currentState) {
        // ...
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // Now that the list is clean, send the didChangeNext/didChangePrevious
  // notifications.
  _flushRouteAnnouncement();
  // Announces route name changes.
  final _RouteEntry lastEntry = _history.lastWhere(_RouteEntry.isPresentPredicate, orElse: () => null);
  final String routeName = lastEntry?.route?.settings?.name;
  if (routeName != _lastAnnouncedRouteName) {
    RouteNotificationMessages.maybeNotifyRouteChange(routeName, _lastAnnouncedRouteName);
    _lastAnnouncedRouteName = routeName;
  }
  // Lastly, removes the overlay entries of all marked entries and disposes
  // them.
  for (final _RouteEntry entry in toBeDisposed) {
    for (final OverlayEntry overlayEntry in entry.route.overlayEntries)
      overlayEntry.remove();
    entry.dispose();
  }
  if (rearrangeOverlay)
    overlay?.rearrange(_allRouteOverlayEntries);
}
複製程式碼

以上是除了狀態處理之外,一次 _flushHistoryUpdates 的全過程,首先它會遍歷整個路由列表,根據狀態做不同的處理,不過一般能夠處理到的也不過最上層一兩個,其餘的多半是直接跳過的。處理完了之後,呼叫 _flushRouteAnnouncement 進行路由之間的前後連結,比如進行動畫的聯動等,

void _flushRouteAnnouncement() {
  int index = _history.length - 1;
  while (index >= 0) {
    final _RouteEntry entry = _history[index];
    if (!entry.suitableForAnnouncement) {
      index -= 1;
      continue;
    }
    final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate);
    if (next?.route != entry.lastAnnouncedNextRoute) {
      if (entry.shouldAnnounceChangeToNext(next?.route)) {
        entry.route.didChangeNext(next?.route);
      }
      entry.lastAnnouncedNextRoute = next?.route;
    }
    final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.suitableForTransitionAnimationPredicate);
    if (previous?.route != entry.lastAnnouncedPreviousRoute) {
      entry.route.didChangePrevious(previous?.route);
      entry.lastAnnouncedPreviousRoute = previous?.route;
    }
    index -= 1;
  }
}
複製程式碼

其實現也比較清晰,對每一個 _RouteEntry,通過呼叫 didChangeNext 和 didChangePrevious 來建立聯絡,比如在 didChangeNext 中繫結當前 Route 的 secondaryAnimation 和下一個路由的 animation 進行動畫聯動,再比如在 didChangePrevious 中獲取上一個路由的 title,這個可以用於 CupertinoNavigationBar 中 back 按鈕展示上一頁面的 title。

然後呼叫 maybeNotifyRouteChange 發出通知,指定當前正在處於展示狀態的 Route。

最後,遍歷 toBeDisposed 執行 _RouteEntry 的銷燬,這個列表會儲存上面迴圈處理過程中,確定需要移出的 _RouteEntry,通過呼叫 OverlayEntry remove 函式(它會將自己從 Overlay 中移除)和 OverlayEntry dispose 函式(它會呼叫 Route 的 dispose,進行資源釋放,比如 TransitionRoute 中 AnimationController 銷燬)。

最後再看關於狀態的處理,以下是所有的狀態:

enum _RouteLifecycle {
  staging, // we will wait for transition delegate to decide what to do with this route.
  //
  // routes that are present:
  //
  add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
  adding, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
  // routes that are ready for transition.
  push, // we'll want to run install, didPush, etc; a route added via push() and friends
  pushReplace, // we'll want to run install, didPush, etc; a route added via pushReplace() and friends
  pushing, // we're waiting for the future from didPush to complete
  replace, // we'll want to run install, didReplace, etc; a route added via replace() and friends
  idle, // route is being harmless
  //
  // routes that are not present:
  //
  // routes that should be included in route announcement and should still listen to transition changes.
  pop, // we'll want to call didPop
  remove, // we'll want to run didReplace/didRemove etc
  // routes should not be included in route announcement but should still listen to transition changes.
  popping, // we're waiting for the route to call finalizeRoute to switch to dispose
  removing, // we are waiting for subsequent routes to be done animating, then will switch to dispose
  // routes that are completely removed from the navigator and overlay.
  dispose, // we will dispose the route momentarily
  disposed, // we have disposed the route
}
複製程式碼

本質上這些狀態分為三類,add(處理初始化的時候直接新增),push(與 add 類似,但是增加了動畫的處理),pop(處理頁面移出),remove(移出某個頁面,相對 pop 沒有動畫,也沒有位置限制)。

add

add 方式新增路由目前還只用於在應用初始化是新增初始化頁面使用,對應的是在 NavigatorState 的 initState 中,

void initState() {
  super.initState();
  for (final NavigatorObserver observer in widget.observers) {
    assert(observer.navigator == null);
    observer._navigator = this;
  }
  String initialRoute = widget.initialRoute;
  if (widget.pages.isNotEmpty) {
    _history.addAll(
      widget.pages.map((Page<dynamic> page) => _RouteEntry(
        page.createRoute(context),
        initialState: _RouteLifecycle.add,
      ))
    );
  } else {
    // If there is no page provided, we will need to provide default route
    // to initialize the navigator.
    initialRoute = initialRoute ?? Navigator.defaultRouteName;
  }
  if (initialRoute != null) {
    _history.addAll(
      widget.onGenerateInitialRoutes(
        this,
        widget.initialRoute ?? Navigator.defaultRouteName
      ).map((Route<dynamic> route) =>
        _RouteEntry(
          route,
          initialState: _RouteLifecycle.add,
        ),
      ),
    );
  }
  _flushHistoryUpdates();
}
複製程式碼

它會將從 onGenerateInitialRoutes 得來的所有初始路由轉成 _RouteEntry 加入到 _history,此時它們的狀態是 _RouteLifecycle.add,然後就是呼叫 _flushHistoryUpdates 進行處理。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
      case _RouteLifecycle.add:
        assert(rearrangeOverlay);
        entry.handleAdd(
          navigator: this,
        );
        assert(entry.currentState == _RouteLifecycle.adding);
        continue;
      case _RouteLifecycle.adding:
        if (canRemoveOrAdd || next == null) {
          entry.didAdd(
            navigator: this,
            previous: previous?.route,
            previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
            isNewFirst: next == null
          );
          assert(entry.currentState == _RouteLifecycle.idle);
          continue;
        }
        break;
      case _RouteLifecycle.idle:
        if (!seenTopActiveRoute && poppedRoute != null)
          entry.handleDidPopNext(poppedRoute);
        seenTopActiveRoute = true;
        // This route is idle, so we are allowed to remove subsequent (earlier)
        // routes that are waiting to be removed silently:
        canRemoveOrAdd = true;
        break;
        // ...
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}
複製程式碼

add 路線主要會呼叫兩個函式,handleAdd 和 didAdd,

void handleAdd({ @required NavigatorState navigator}) {
  assert(currentState == _RouteLifecycle.add);
  assert(navigator != null);
  assert(navigator._debugLocked);
  assert(route._navigator == null);
  route._navigator = navigator;
  route.install();
  assert(route.overlayEntries.isNotEmpty);
  currentState = _RouteLifecycle.adding;
}
複製程式碼

install 函式可以看作是 Route 的初始化函式,比如在 ModalRoute 中建立 ProxyAnimation 來管理一些動畫的執行,在 TransitionRoute 中建立了用於執行切換動畫的 AnimationController,在 OverlayRoute 中完成了當前 Route 的 OverlayEntry 的建立及插入。createOverlayEntries 用於建立 OverlayEntry,其實現在 ModalRoute,

Iterable<OverlayEntry> createOverlayEntries() sync* {
  yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
  yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
複製程式碼

每一個 Route 都能生成兩個 OverlayEntry,一個是 _buildModalBarrier,它可以生成兩個頁面之間的屏障,我們可以利用它給新頁面設定一個背景色,同時還支援動畫過渡,另一個是 _buildModalScope,它生成的就是這個頁面真正的內容,外部會有多層包裝,最底層就是 WidgetBuilder 建立的 widget。

大致看下兩個函式的實現,

Widget _buildModalBarrier(BuildContext context) {
  Widget barrier;
  if (barrierColor != null && !offstage) { // changedInternalState is called if these update
    assert(barrierColor != _kTransparent);
    final Animation<Color> color = animation.drive(
      ColorTween(
        begin: _kTransparent,
        end: barrierColor, // changedInternalState is called if this updates
      ).chain(_easeCurveTween),
    );
    barrier = AnimatedModalBarrier(
      color: color,
      dismissible: barrierDismissible, // changedInternalState is called if this updates
      semanticsLabel: barrierLabel, // changedInternalState is called if this updates
      barrierSemanticsDismissible: semanticsDismissible,
    );
  } else {
    barrier = ModalBarrier(
      dismissible: barrierDismissible, // changedInternalState is called if this updates
      semanticsLabel: barrierLabel, // changedInternalState is called if this updates
      barrierSemanticsDismissible: semanticsDismissible,
    );
  }
  return IgnorePointer(
    ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates
              animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
    child: barrier,
  );
}
複製程式碼

ModalBarrier 是兩個 Route 之間的屏障,它可以通過顏色、攔截事件來表示兩個 Route 的隔離,這些都是可以配置的,這裡 IgnorePointer 的作用是為了在執行切換動畫的時候無法響應時間。

Widget _buildModalScope(BuildContext context) {
  return _modalScopeCache ??= _ModalScope<T>(
    key: _scopeKey,
    route: this,
    // _ModalScope calls buildTransitions() and buildChild(), defined above
  );
}

Widget build(BuildContext context) {
  return _ModalScopeStatus(
    route: widget.route,
    isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
    canPop: widget.route.canPop, // _routeSetState is called if this updates
    child: Offstage(
      offstage: widget.route.offstage, // _routeSetState is called if this updates
      child: PageStorage(
        bucket: widget.route._storageBucket, // immutable
        child: FocusScope(
          node: focusScopeNode, // immutable
          child: RepaintBoundary(
            child: AnimatedBuilder(
              animation: _listenable, // immutable
              builder: (BuildContext context, Widget child) {
                return widget.route.buildTransitions(
                  context,
                  widget.route.animation,
                  widget.route.secondaryAnimation,
                  IgnorePointer(
                    ignoring: widget.route.animation?.status == AnimationStatus.reverse,
                    child: child,
                  ),
                );
              },
              child: _page ??= RepaintBoundary(
                key: widget.route._subtreeKey, // immutable
                child: Builder(
                  builder: (BuildContext context) {
                    return widget.route.buildPage(
                      context,
                      widget.route.animation,
                      widget.route.secondaryAnimation,
                    );
                  },
                ),
              ),
            ),
          ),
        ),
      ),
    ),
  );
}
複製程式碼

_ModalScope 需要承載使用者介面的展示,它的 build 函式可以看到在 widget.route.buildPage 出使用者定義的頁面之上有很多層,可以一層一層看下大致作用:

  1. _ModalScopeStatus,繼承自 InheritedWidget,用於給底層結點提供資料
  2. Offstage,可以通過 offstage 變數控制是否繪製
  3. PageStorage,它提供了一種儲存策略,也就是 PageStorageBucket,這個類可以給某一個 BuildContext 繫結特定的資料,支援寫入和讀取,可用於某一個 widget 的狀態儲存等
  4. FocusScope,用於焦點管理用,一般只有獲取焦點的控制元件才能接收到按鍵資訊等
  5. RepaintBoundary,控制重繪範圍,意在減少不必要的重繪
  6. AnimatedBuilder,動畫控制 Widget,會根據 animation 進行 rebuild
  7. widget.route.buildTransitions,它在不同的 Route 中可以有不同的實現,比如 Android 的預設實現是自下向上漸入,ios 的預設實現是自右向左滑動,另外也可以通過自定義 Route 或自定義 ThemeData 實現自定義的切換動畫,還有一點需要說明,Route 中的動畫分為 animation 和 secondaryAnimation,其中 animation 定義了自己 push 時的動畫,secondaryAnimation 定義的是新頁面 push 時自己的動畫,舉個例子,在 ios 風格中,新頁面自右向左滑動,上一個頁面也會滑動,此時控制上一個頁面滑動的動畫就是 secondaryAnimation
  8. IgnorePointer,同樣是用於頁面切換動畫執行中,禁止使用者操作
  9. RepaintBoundary,這裡的考量應該是考慮到上層有一個動畫執行,所以這裡包一下避免固定內容重繪
  10. Builder,Builder 的唯一作用應該是提供 BuildContext,雖然說每一個 build 函式都有 BuildContext 引數,但這個是當前 Widget 的,而不是直屬上級的,這可能有點抽象,比如說下面的 buildPage 需要使用 BuildContext 作為引數,那麼如果它需要使用 context 的 ancestorStateOfType 的話,實際上就是從 _ModalScopeState 開始向上查詢,而不是從 Builder 開始向上查詢
  11. widget.route.buildPage,這個函式內部就是使用 Route 的 WidgetBuilder 建立使用者介面,當然不同的 Route 可能還會在這裡再次進行包裝

以上就是一個頁面中,從 Overlay(說是 Overlay 不是那麼合理,但是在此先省略中間的 _Theatre 等) 往下的佈局巢狀。新的 OverlayEntry 建立完成之後,會把它們都傳遞到 Overlay 中,且在這個過程中會呼叫 Overlay 的 setState 函式,請求重新繪製,在 Overlay 中實現新舊頁面的切換。

以上是 install 的整個過程,執行完了之後把 currentState 置為 adding 返回。

此處有一點需要注意,while 迴圈會自上往下遍歷所有的 _RouteEntry,但是當一個連續操作尚未完成時,它是不會去執行下一個 _RouteEntry 的,其實現就在於程式碼中的 continue 關鍵字,這個關鍵字會直接返回執行下一次迴圈,但是並沒有更新當前 _RouteEntry,所以實際處理的還是同一個路由,這種一般用於 _RouteEntry 狀態發生變化,且需要連續處理的時候,所以對於 add 來說,執行完了之後會立刻執行 adding 程式碼塊,也就是 didAdd,

void didAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) {
  route.didAdd();
  currentState = _RouteLifecycle.idle;
  if (isNewFirst) {
    route.didChangeNext(null);
  }
  for (final NavigatorObserver observer in navigator.widget.observers)
    observer.didPush(route, previousPresent);
}
複製程式碼

Route 的 didAdd 函式表示這個路由已經新增完成,它會做一些收尾處理,比如在 TransitionRoute 中更新 AnimationController 的值到最大,並設定透明等。然後 didAdd 將狀態置為 idle,並呼叫所有監聽者的 didPush。idle 表示一個 _RouteEntry 已經處理完畢,後續只有 pop、replace 等操作才會需要重新處理,add 過程到這裡也可以結束了。

push

Future<T> push<T extends Object>(Route<T> route) {
  assert(!_debugLocked);
  assert(() {
    _debugLocked = true;
    return true;
  }());
  assert(route != null);
  assert(route._navigator == null);
  _history.add(_RouteEntry(route, initialState: _RouteLifecycle.push));
  _flushHistoryUpdates();
  assert(() {
    _debugLocked = false;
    return true;
  }());
  _afterNavigation(route);
  return route.popped;
}
複製程式碼

push 過程就是將 Route 封裝成 _RouteEntry 加入到 _history 中並呼叫 _flushHistoryUpdates,它的初始狀態時 push,並在最後返回 route.popped,這是一個 Future 物件,可以用於前一個頁面接收新的頁面的返回結果,這個值是在當前路由 pop 的時候傳遞的。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
      // ...
      case _RouteLifecycle.push:
      case _RouteLifecycle.pushReplace:
      case _RouteLifecycle.replace:
        assert(rearrangeOverlay);
        entry.handlePush(
          navigator: this,
          previous: previous?.route,
          previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
          isNewFirst: next == null,
        );
        assert(entry.currentState != _RouteLifecycle.push);
        assert(entry.currentState != _RouteLifecycle.pushReplace);
        assert(entry.currentState != _RouteLifecycle.replace);
        if (entry.currentState == _RouteLifecycle.idle) {
          continue;
        }
        break;
      case _RouteLifecycle.pushing: // Will exit this state when animation completes.
        if (!seenTopActiveRoute && poppedRoute != null)
          entry.handleDidPopNext(poppedRoute);
        seenTopActiveRoute = true;
        break;
      case _RouteLifecycle.idle:
        if (!seenTopActiveRoute && poppedRoute != null)
          entry.handleDidPopNext(poppedRoute);
        seenTopActiveRoute = true;
        // This route is idle, so we are allowed to remove subsequent (earlier)
        // routes that are waiting to be removed silently:
        canRemoveOrAdd = true;
        break;
      // ...
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}
複製程式碼

這裡將 push、pushReplace、replace 都歸為了一類,它會先呼叫 handlePush,這個函式中其實包含了 add 過程中的 handleAdd、didAdd 兩個函式的功能,比如呼叫 install、呼叫 didPush,不同的是,push/pushReplace 會有一個過渡的過程,即先執行切換動畫,此時它的狀態會變為 pushing,並在動畫執行完時切到 idle 狀態並呼叫 _flushHistoryUpdates 更新,而 replace 則直接呼叫 didReplace 完成頁面替換,從這裡看,這個應該是沒有動畫過渡的。後面還是一樣,呼叫通知函式。

pop

pop 的過程與上面兩個不太一樣,它在 NavigatorState.pop 中也有一些操作:

void pop<T extends Object>([ T result ]) {
  assert(!_debugLocked);
  assert(() {
    _debugLocked = true;
    return true;
  }());
  final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
  if (entry.hasPage) {
    if (widget.onPopPage(entry.route, result))
      entry.currentState = _RouteLifecycle.pop;
  } else {
    entry.pop<T>(result);
  }
  if (entry.currentState == _RouteLifecycle.pop) {
    // Flush the history if the route actually wants to be popped (the pop
    // wasn't handled internally).
    _flushHistoryUpdates(rearrangeOverlay: false);
    assert(entry.route._popCompleter.isCompleted);
  }
  assert(() {
    _debugLocked = false;
    return true;
  }());
  _afterNavigation<dynamic>(entry.route);
}
複製程式碼

就是呼叫 _RouteEntry 的 pop,在這個函式中它會呼叫 Route 的 didPop,完成返回值的傳遞、移出動畫啟動等。但是在 OverlayRoute 中:

bool didPop(T result) {
  final bool returnValue = super.didPop(result);
  assert(returnValue);
  if (finishedWhenPopped)
    navigator.finalizeRoute(this);
  return returnValue;
}
複製程式碼

finalizeRoute 的呼叫需要依賴 finishedWhenPopped 的值,這個值在子類中可以被修改,比如 TransitionRoute 中它就是 false,理解也很簡單,在 TransitionRoute 中執行 didPop 之後也不能直接就銷燬 Route,而是先要執行移出動畫,而如果不需要執行動畫,則可以直接呼叫,否則就在動畫執行完再執行,這一點是通過監聽動畫狀態實現的,在 TransitionRoute 中。

void finalizeRoute(Route<dynamic> route) {
  // FinalizeRoute may have been called while we were already locked as a
  // responds to route.didPop(). Make sure to leave in the state we were in
  // before the call.
  bool wasDebugLocked;
  assert(() { wasDebugLocked = _debugLocked; _debugLocked = true; return true; }());
  assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);
  final _RouteEntry entry =  _history.firstWhere(_RouteEntry.isRoutePredicate(route));
  if (entry.doingPop) {
    // We were called synchronously from Route.didPop(), but didn't process
    // the pop yet. Let's do that now before finalizing.
    entry.currentState = _RouteLifecycle.pop;
    _flushHistoryUpdates(rearrangeOverlay: false);
  }
  assert(entry.currentState != _RouteLifecycle.pop);
  entry.finalize();
  _flushHistoryUpdates(rearrangeOverlay: false);
  assert(() { _debugLocked = wasDebugLocked; return true; }());
}
複製程式碼

在 finalizeRoute 中,它會判斷是否正在 pop 過程中,如果是,就說明此刻是直接呼叫的 finalizeRoute,那就需要先執行 pop 狀態的操作,再執行 dispose 操作,將狀態切換到 dispose 進行處理,如果不是,就說明呼叫這個函式的時候,是動畫執行完的時候,那麼此刻 pop 狀態處理已經完成,所以跳過了 pop 處理的步驟,如上。下面就看一下 pop 過程做的處理。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
      // ...
      case _RouteLifecycle.pop:
        if (!seenTopActiveRoute) {
          if (poppedRoute != null)
            entry.handleDidPopNext(poppedRoute);
          poppedRoute = entry.route;
        }
        entry.handlePop(
          navigator: this,
          previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
        );
        assert(entry.currentState == _RouteLifecycle.popping);
        canRemoveOrAdd = true;
        break;
      case _RouteLifecycle.popping:
        // Will exit this state when animation completes.
        break;
      case _RouteLifecycle.dispose:
        // Delay disposal until didChangeNext/didChangePrevious have been sent.
        toBeDisposed.add(_history.removeAt(index));
        entry = next;
        break;
      case _RouteLifecycle.disposed:
      case _RouteLifecycle.staging:
        assert(false);
        break;
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}
複製程式碼

handlePop 將狀態切換到 poping(動畫執行過程),然後發出通知,而 poping 狀態不作處理,因為這是一個過渡狀態,在動畫執行完之後會自動切換到 dispose 狀態,同樣的,上面的 pushing 狀態也是,而在 dispose 分支中,就是將 _RouteEntry 從 _history 移除並加入到 toBeDisposed,然後在遍歷結束之後統一銷燬。

remove

remove 的邏輯就是先從 _history 中找到一個跟傳進來的一致的 _RouteEntry,將它的狀態設為 remvoe,再呼叫 _flushHistoryUpdates。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
        // ...
      case _RouteLifecycle.remove:
        if (!seenTopActiveRoute) {
          if (poppedRoute != null)
            entry.route.didPopNext(poppedRoute);
          poppedRoute = null;
        }
        entry.handleRemoval(
          navigator: this,
          previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
        );
        assert(entry.currentState == _RouteLifecycle.removing);
        continue;
      case _RouteLifecycle.removing:
        if (!canRemoveOrAdd && next != null) {
          // We aren't allowed to remove this route yet.
          break;
        }
        entry.currentState = _RouteLifecycle.dispose;
        continue;
      case _RouteLifecycle.dispose:
        // Delay disposal until didChangeNext/didChangePrevious have been sent.
        toBeDisposed.add(_history.removeAt(index));
        entry = next;
        break;
      case _RouteLifecycle.disposed:
      case _RouteLifecycle.staging:
        assert(false);
        break;
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}
複製程式碼

首先會呼叫 handleRemoval,呼叫通知,並將狀態切換到 removing,在 removing 階段再將狀態切到 dispose,然後就是將其加入 toBeDisposed,所以整個過程中是不涉及動畫的,一般只用來移出非正在展示的頁面,否則還是推薦用 pop。

總結

以上是路由機制的實現原理,就其整體而言,最給人耳目一新的就是狀態管理的加入,通過將一個頁面的進出劃分到不同狀態處理,是能夠有效降低程式碼的複雜度的,不過從目前的結果來看,這一個過程執行的還不夠精煉,比如狀態的劃分不夠合理,從這些狀態的設計來看,add/push/pop 都有對應的 ing 形式表示正在執行中,但是 adding 的存在我暫時沒有看到必要性,還有就是感覺程式碼的組織上還是有點問題,比如 handleAdd 與 handPush 實際上還有很大部分的程式碼重複的,這部分不知道以後會不會優化。

另外還有一點感覺做的不到位,就是 _routeNamed 這個函式沒有對外開放,而且並不是所有的路由操作都提供了 name 為入參的包裝,比如 removeRoute,在這種情況下就沒法很方便的呼叫。

相關文章