flutter 路由機制

參考代碼版本,1.18node

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

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

實現基礎

flutter 應用的運行須要依賴 MaterialApp/CupertinoApp 這兩個 Widget,他們分別對應着 android/ios 的設計風格,同時也爲應用的運行提供了一些基本的設施,好比與路由相關的主頁面、路由表等,再好比跟總體頁面展現相關的 theme、locale 等。markdown

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

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

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,在這種狀況下就無法很方便的調用。

相關文章
相關標籤/搜索