敢問路在何方——Flutter 路由淺析

在開始以前,咱們先介紹一個貌似絕不相關的概念,至於緣由,一是由於後面的某個概念和它具備相關性,二是由於這個概念太簡單,不足以以一整篇篇幅來介紹它,因此不如就在這裏順帶着介紹一下。api

InheritedWidget

一句話總結 InheritedWidget 就是「在視圖樹上更有效的向下傳遞信息的 widget」。瀏覽器

abstract class InheritedWidget extends ProxyWidget {
  const InheritedWidget({ Key key, Widget child })
    : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
複製代碼

updateShouldNotify() 方法用來控制對其實現的子類是否在 rebuild 過程當中一樣進行 rebuild,例如,當此 widget 的數據並未改變時,可能並不須要對其進行更新。markdown

因此,相比於通常的 widget,它主要多了個在視圖樹上實現「信息傳遞」的功能,那它的信息傳遞的功能又是如何實現的呢——藉助 BuildContext 類,咱們線看一個例子。app

class FrogColor extends InheritedWidget {
  const FrogColor({
    Key key,
    @required this.color,
    @required Widget child,
  }) : assert(color != null),
       assert(child != null),
       super(key: key, child: child);

  final Color color;

  static FrogColor of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<FrogColor>();
  }

  @override
  bool updateShouldNotify(FrogColor old) => color != old.color;
}
複製代碼

of() 方法接收 BuildContext 參數,並返回參數的 dependOnInheritedWidgetOfExactType() 方法調用結果,而該方法的實如今 BuildContext 類的子類 Element 類中。ide

inherited_widget.jpg

這個從視圖樹上按名稱摘果子的過程並不難理解。好了,關於 InheritedWidget 的部分咱們就瞭解這麼多,下面迴歸本篇的核心主旨——路由部分。函數

從 Navigator 1.0 開始

Navigator 以棧的方式管理着它的家族控件們。正如在 Android 中經過一個棧來管理 Activity,每一個 Activity 做爲一個單獨的頁面的原則, Flutter 中也以棧的方式管理着咱們須要的頁面,不過每一個頁面再也不是 Activity,而變成了 route。oop

說到 Navigator,咱們能夠在腦海中造成這樣一種畫像,在桌子上摞着一疊圖紙,咱們能看到最上面的那一張畫了些什麼,可是沒法其餘在下面的圖紙的內容。若是如今把最上面的那張圖紙拿開,原先自上而下的第二張此刻就變成了最上面的那張圖紙,此時咱們看到畫像就仍是新的最上面的那幅。那再放置一張新的畫像在這一摞圖畫之上,可見的圖畫就又被更新了。post

olia-gozha-prhiWWrS-SE-unsplash.jpg

當咱們須要添加新的「圖畫」時,只須要使用 Navigatorpush 系列方法就能夠了,push 系列方法有好些個,忽略其餘的附加操做,它們能夠分爲兩類—— pushpushNamed學習

image-20210303001249950.png

1.push

Navigator.of(context).push(MaterialPageRoute(builder: (context) => SignUpPage()));
複製代碼

這句代碼並不特別,是跳轉一個新界面的通常作法。MaterialPageRouteRoute 的子類,builder 參數返回新界面的 Widget 實例。ui

NavigatorStatefulWidget 的子類,對應的 StateNavigatorStateNavigator.of(context) 方法返回 NavigatorState 實例,和上面 InheritedWidget 相似,藉助 BuildContextfindRootAncestorStateOfType() 方法在 Element 樹上尋找對應的 StatefulElement,返回能夠和泛型指定的 State 類型匹配的 State 對象。

在深刻 push() 方法以前,咱們先借助 devtools 瞭解一下 Flutter 頁面的層級結構,可以幫助咱們更好地理解下面的流程。

因爲圖片太長,請點擊查看

接下來就是調用 Navigatorpush() 方法了,這部分的邏輯比較複雜,我嘗試着按個人理解繪製了一張流程圖,對照着來理解整個過程。

graph TB
	A["push()"] --> B["_history.add()"]
	A --> C["_flushHistoryUpdates"]
	A --> D["_afterNavigation()"]
	C --> E["entry.handlePush()"]
	C --> F["overlay.rearrange(_allRouteOverlayEntries)"]
	E --> G["entry.handlePush()"]
	G --> H["_overlayEntries.addAll(createOverlayEntries())"]
	H --> I["_buildModalBarrier()"]
	H --> J["_buildModalScope()"]
	J --> K["_ModalScope<_ModalScopeState>"]
	K --> L["build()"]
	L --> M["ModalRoute.buildPage()"]
	M --> N["MaterialPageRoute.builder"]
	D --> O["_cacelActivePointers()"]
	O --> P["setState()"]
	P --> Q["build()"]
	Q --> R["Overlay.initialEntries = _allRouteOverlayEntries"]
	S{{"for (entry in _history) yield entry.route.overlayEntries"}}
	F -->|"_allRouteOverlayEntries"| S
	H -->|"_overlayEntries"| S
	F -.-|"_allRouteOverlayEntries"| R

push() 方法接受 Route 型的參數,並在方法內將其封裝爲 _RouteEntry 型。Navigator 類有一個成員 _history,是一個 OverlayEntry 對象的集合,push() 方法將封裝好的 _RouteEntry 對象添加到 _history 列表中。以後 push() 方法調用 _RouteEntryhandlePush() 方法,建立 「_ModalBarrir」 和 「_ModalScope」,它們都是 Widget 對象,前者是用來隔毫不同界面之間的交互操做(例如手勢操做),後者是對咱們目標跳轉頁面的封裝。最後 push() 方法調用 _afterNavigation() 方法刷新 Navigator,導致 build() 方法被調用,在此方法中,Navigator 經過 GlobalKey 獲取到全局的 Overlay 對象,並將被 _OverlayEntryWidget 對象包裹的 「_ModalScope」頁面更新到 Overlay 中,這樣咱們的界面就能夠顯示在頁面層級中了。

2. pushNamed

MaterialApp 中支持經過 onGenerateRoute 參數來構建路由表。它是一個方法,形式爲 Route<dynamic> Function(RouteSettings settings),根據傳入的 RouteSettings 對象參數,返回對應的 Route 實例。RouteSettings 類擁有兩個成員變量分別爲 final String namefinal Object arguments。而 NavigatorNavigatorStatepushNamed() 方法參數接收的正是這兩個對象。

Future<T> pushNamed<T extends Object>(
    String routeName, {
        Object arguments,
    }) {
    return push<T>(_routeNamed<T>(routeName, arguments: arguments));
}
複製代碼

能夠看到 pushNamed() 方法最終調用仍是上面介紹的 push() 方法,可是參數則經過 _routeName() 方法來構建。

Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) {
    
    // ...
    
    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;
}
複製代碼

使用 flutter 命令行運行 flutter run --route=/signup 查看 demo。

3. pop

當調用 pop() 方法時,會將頁面棧早上層的頁面視圖彈出,顯示出下面一張的視圖。

在這個過程當中,_flushHistoryUpdates() 方法依然發揮着重要的做用,經過 _RouteEntry.currentState 變量控制彈出的過程,分別爲poppopingremoveremovingdisposedisposed,並在這些過程當中移除 NavigatorState._history 中的對應的 _RouteEntry 實例,在刷新視圖時,Overlay 獲得更新,被移除的實例會將包裹的頁面移除 Overlay 層。

那麼在這個過程當中,前一個頁面的數據是如何傳遞到後一個頁面的呢?

graph LR
	A["NavigatorState.pop(result)"] --> B["_RouteEntry.pop(result)"] -->
    C["Route.didPop(result)"] --> D["Route.didComplete(result)"]

在通過上面的調用後,pop() 方法的參數 result 被傳遞到 Route.didComplete() 方法。

void didComplete(T result) {
    _popCompleter.complete(result ?? currentResult);
}
複製代碼

_popCompleter 對象是 Completer 類的實例,而 _popCompleterfuture 屬性在 NavigatorState.push() 方法調用時被返回。

/// Route
Future<T> get popped => _popCompleter.future;

/// NavigatorState
Future<T> push<T extends Object>(Route<T> route) {
    // ...
    return route.popped;
}
複製代碼

因此後一個頁面調用 pop() 方法返回的結果能被前一個頁面在調用 push() 方法後以 Future 的形式接收到,諸以下面的形式:

Navigator.of(context).push(MaterialPageRoute(builder: (context) => SignUpPage()))
    .then((value) => print("the result from next page: $value"));
複製代碼

再到 Navigator 2.0

1、用法介紹

Navigator 到目前爲止,一切都運行良好,可是它的侷限也很明顯。首先,它沒法一次性壓入多個頁面;其次,它只能彈出最上層的頁面,對於某些場景下彈出下層頁面的需求則沒法知足。因此 Navigator 2.0 就應運而生了。

在 Flutter 迭代到 1.22 版本後,關於 Navigator 的部分添加了一些新的 api:

  • Page —— 抽象的「頁面」的概念,對 Route 配置選項的一種描述;
  • Router —— 管家角色,應用中頁面打開或關閉的調度員,監聽來自於系統的路由信息(如啓動路由、新路由加入或者系統返回按鈕的消息等);
  • RouteInformationProvider —— 更改路由獲取到的頁面的名字;
  • RouteInfomationParser —— 接收來自 RouteInfomationProviderRouteInfomation 並將其轉化爲泛型約束的數據類型;
  • RouterDelegate —— 輸入來自 RouteInformationParser 的數據,負責將提供的 navigator 頁面插入視圖樹,同時接受監聽更新視圖;
  • BackButtonDispatcher —— 監聽返回按鈕事件。

Navigator 2.0 的概念和以前介紹過的 Flutter 視圖樹比較類似——Widget 保存着視圖的配置,經過 Widget 對象建立對應的 ElementRenderObject——Page 對象是關於路由的配置的抽象的概念,而經過它的 createRoute() 方法建立 Route 對象。

1. Page

Page 是一個頁面的抽象,繼承自 RouteSettings 類,經過 name 屬性來標識頁面。正如 WidgetElement 經過 createElement() 方法,Page 中也有一個方法 createRoute() 用來建立 Route 實例。

經過上面 Navigator 1.0 的分析,咱們知道 Route 是 Flutter 路由進行頁面切換的載體,包裹着真正的頁面在棧中「騰挪閃轉」,從而實現頁面切換的功能。

2. RouterInformationProvider

這個類經過它的 value 屬性傳遞值給 RouteInformationParser 類的 parseRouteInformation 方法,該值即 RouteInformation 對象,儲存路由的地址,經過該地址能夠控制頁面跳轉。

例如當咱們在瀏覽器的地址欄輸入 「/index」後綴做爲新的跳轉地址後,RouteInformationParser 類的 parseRouteInformation 方法便可接收到 location 屬性存儲有 「/index」值的 RouteInformation 對象。

3. RouteInformationParser

該類提供了兩個方法,分別是 parseRouteInformationrestoreRouteInformation

parseRouteInformation 方法接收地址信息—— RouteInformation ,而後返回 Future<T> 類型對象,「T」是一個約定的任意類型,返回的 Future<T> 類型將在 RouterDelegate 類的 setNewRoutePath 方法被接收,能夠在該方法中真正實現頁面添加跳轉的邏輯。

一般該方法的調用來自瀏覽器地址欄輸入地址後跳轉,而咱們經過 navigator 實現的界面跳轉不會致使該方法被調用。

restoreRouteInformation 方法用來恢復瀏覽歷史頁面,好比咱們須要作「前進」或「後退」的功能而保持瀏覽器地址欄中的地址不變,則能夠經過 Router 類的 navigate() 方法強制上報路由信息從而觸發該方法。該方法返回的 RouteInformation 對象被 parseRouteInformation 方法接收和處理。

4. RouterDelegate

該類是處理路由地址的主要類,頁面的壓入與彈出都在這個類中進行。

首先,這個類經過 setNewRoutePath 方法接收新的路由地址,而後對新的地址進行查找(通常在用戶本身維護的路由表中),將對應的頁面壓入棧。其次,該類提供了 build 方法,Router 對象會調用該方法獲取視圖樹對象,因此該方法中應當返回能表明當前視圖樹的 Widget 對象,以供系統對顯示視圖進行更新。

5. Router

管理頁面的管家。它不只負責頁面的構建,還負責業務邏輯的處理與分發。

上面介紹到 Navigator 2.0 的思想在於把一部分的頁面棧的操做權限下放給用戶,在 App 中,若是咱們須要對頁面棧進行排序、插入、多頁面插入、刪除、多頁面刪除,或者對瀏覽器更新與加載方式等進行操做時,須要用到上面介紹的一些對象,這些對象都在 Router 中持有引用,因此咱們就可使用 Router 對象獲取到這些對象的引用,而 Router 對象能夠經過其靜態方法 of() 獲取。

大體的介紹就這麼多,用法能夠看這個 demo。下面簡單串一下系統的運行流程。

2、原理分析

首先 MaterialApp.router() 構造方法會傳入 routeInformationParserrouterDelegate 等對象,_MateiralAppState 對象在 build() 方法中調用 _buildWidgetApp() 方法構造 WidgetsApp 對象,由於 routerDelegate 對象是必填字段,因此 bool get _usesRouter => widget.routerDelegate != null; 字段爲 true,會經過 WidgetsApp.router 構造函數構造,而後在 _WidgetsAppState 類的 build() 方法中構造 Router 對象,因此它的層級結構以下(固然,它們之間還穿插着其餘的包裝類):

router.png

Router 類繼承自 StatefulWidget,那麼老規矩,仍是看 _RouterStatebuild() 方法:

Widget build(BuildContext context) {
  return _RouterScope(
    routeInformationProvider: widget.routeInformationProvider,
    backButtonDispatcher: widget.backButtonDispatcher,
    routeInformationParser: widget.routeInformationParser,
    routerDelegate: widget.routerDelegate,
    routerState: this,
    child: Builder(
      // We use a Builder so that the build method below
      // will have a BuildContext that contains the _RouterScope.
      builder: widget.routerDelegate.build,
    ),
  );
}
複製代碼

可見,最終仍是會調用 RouterDelegatebuild() 方法來建立頁面,該方法由開發者實現。

咱們對該方法的實現以下:

@override
Widget build(BuildContext context) {
    return Navigator(
        key: navigatorKey,
        pages: List.of(_pages),
        onPopPage: (route, result) {
            if (_pages.length > 1 && route.settings is MyPage) {
                final MyPage<dynamic>? removed = _pages.lastWhere(
                    (element) => element.name == route.settings.name,
                );
                if (removed != null) {
                    _pages.remove(removed);
                    notifyListeners();
                }
            }

            return route.didPop(result);
        },
    );
}
複製代碼

Navigator 都很熟悉了,可是這裏的用法又和上面介紹的兩種用法都不同。

這裏經過 Navigatorpages 屬性,將頁面列表 List<Page> 傳遞進去,當視圖配置有變動時,觸發視圖更新,此方法被調用,而後經過比較 pages 是否已產生變化,來決定是否更新頁面,最終會調用 Navigator_updatePages 方法。這個方法的內容有點多,咱們就不作具體說明了,只大概說一下它的工做流程。

這個方法比較新的 pages 列表和舊的 _history 列表(元素爲 _RouteEntry 類型),而後產生新的 _history 列表。這個方法大體和 RenderObjectElement.updateChildren() 方法流程相同。

須要注意的是,這個方法全程在圍繞着兩個列表進行——舊的路由列表 _history 以及新的頁面列表 widget.pages,咱們把前者稱爲「oldEntries」,把後者稱爲 「newPages」,經過兩個列表共同比對,剔除 oldEntries 中非 Page 型的節點,而用 newPages 中的節點更新對應的 oldEntries 的節點。

  1. 首先從 List 頭開始同步節點,並記錄非 Page 的路由,直到匹配完全部的節點。

  2. 從 List 尾部開始遍歷,但不一樣步節點,直到再也不有匹配的節點,而後最後同步全部的節點,之因此這麼作,是由於咱們想以從頭至尾的順序來同步這些節點。此時,咱們將舊 List 和新 List 縮小到節點再也不匹配的位置。

  3. 遍歷舊列表被收縮的部分,得到一個存儲 Key 值的 List。

  4. 正向遍歷新 List 被收縮的部分(即去除已遍歷兩端的中間部分):

    • 對無 Key 元素建立 _RouteEntry 對象並將其記錄爲 transitionDelegate(轉場頁面);
    • 同步有 Key 的元素列表(若是存在的話)。
  5. 再次遍歷舊 List 被收縮的部分,並記錄 _RouteEntry 和非 Page 路由(須要從 transitionDelegate 中被移除)。

  6. 從列表尾部再次遍歷,同步節點狀態,並記錄非 Page 頁面。

  7. 根據 transitionDelegate 配置轉場效果。

  8. 將非 Page 路由從新填充回新的 _history

更新過 _history 以後,剩下的流程就和 Navigator 1.0 中介紹的相同了——經過 Overlay 對象更新頁面棧,完成頁面顯示和切換的需求。

Navigator 2.0 的思路就是將頁面的排列和更替經過一個 Page 列表—— pages 徹底交給開發者,開發者只須要維護好 pages,轉化爲真正可顯示的界面的過程就交給 Flutter engine 便可。


  1. 本文中關於 Navigator 2.0 的部分理解學習了這篇文章,demo 也是根據文章中的 demo 參考而得。
相關文章
相關標籤/搜索