Flutter 必知必會系列 —— 探索 Route 頁面開啟過程

語言: CN / TW / HK

前面我們已經介紹了 OverlayRoute等點, 為頁面疊加做了完全的準備,這一節我們就解析最常用的一段程式碼 Navigator.push。和 Navigator 1 相比,Navigator 2 更加宣告式,增加了 Page 等API,後面我會專門把官方的 Navigator 2 的設計原則翻譯出來,這一節只跟蹤 Navigatorpush 過程串聯起來前面的關鍵點。

往期精彩

👉 Flutter 必知必會系列 —— Navigator 的開始 Overlay

👉 Flutter 必知必會系列 —— 無名英雄 _Theatre 舞臺劇

👉 Flutter 必知必會系列 —— 全面認識 Route 路由

路由操作的方式

我們的路由操作基本分為三類:開啟、關閉、替換。對應到Navigator 的 API 就是 pushpopreplace。\ 每一類又根據操作的方式分為:直接間接,直接的方式就是直接操作 Route,間接的方式就是通過名字來操作 Route

整體的 API 方法如下:

路由操作API.png

我們最常用的 API 可能就是 pushpoppushpop 是一對相反的操作,所以我們只跟蹤 push 過程即可。

新增路由

我們常用的直接新增路由的方式如下:

```dart Navigator.push(context, MaterialPageRoute(builder: (context) { return const Text("頁面或者對話方塊"); }));

Navigator.of(context).push(MaterialPageRoute(builder: (context) { return const Text("頁面或者對話方塊"); }));

```

我們直接告訴 Navigator 下一個路由是什麼,然後 Navigator 就開始了它的顯示流程。

間接新增路由的方式:

```dart Navigator.pushNamed(context, "路由名字");

```

我們告訴 Navigator 路由的名字是什麼,Navigator 就會在前期註冊的路由表中查名字對應的路由是什麼,然後 Navigator 才會開始它的顯示流程。

這種間接的方式更加靈活,可以在查名字的時候增加路由攔截。

下面以直接的方式為例,跟蹤它的顯示流程。

Navigator.push 推入路由

dart Navigator.push(context, route)

pushNavigator 中的靜態方法,這種寫法和很多系統的元件相似,其方法內部是:

```dart @optionalTypeArgs static Future push(BuildContext context, Route route) { return Navigator.of(context).push(route); //第一處 }

static NavigatorState of( BuildContext context, { bool rootNavigator = false, }) { NavigatorState? navigator; if (context is StatefulElement && context.state is NavigatorState) { navigator = context.state as NavigatorState; } if (rootNavigator) { //第二處 navigator = context.findRootAncestorStateOfType() ?? navigator; } else { navigator = navigator ?? context.findAncestorStateOfType(); } return navigator!; } ```

我們看第一處的程式碼,直接呼叫了 Navigator.ofNavigatorStatefulWidget,它的邏輯都在其 State 中 ———— NavigatorStateof 方法就是返回 NavigatorState

我們再看第二處,注意 rootNavigator 的值,它代表了是否返回最頂層的 NavigatorState,如果是 false,表示向上查詢最近的 NavigatorState,如果是 true,表示向上找到最頂層的 NavigatorState

回過頭看第一處的程式碼,rootNavigator 是 false 的,表示只要向上找到最近的 NavigatorState 就可以,我們以下面的例子為例:

navigator 節點.png

如果是在 G 節點呼叫 Navigator.of(context) 方法,返回的就是 C 節點,如果呼叫的是 Navigator.of(context, rootNavigator: true) 返回的節點就是 A。\ 同樣,在 B 節點向上查詢的時候,不管是不是使用 rootNavigator 都會返回 A 節點

上面就是頁面開啟的第一步,找到管理路由的 NavigatorState,接下來我們看 NavigatorStatepush 操作。

NavigatorState 中的 push

@optionalTypeArgs Future<T?> push<T extends Object?>(Route<T> route) { _pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push)); // 第一處 return route.popped; } 和大多數 API 一樣,Route 也有包裝的過程,將我們傳入的 MaterialPageRoute 包裝成 _RouteEntry,然後執行 _pushEntry 的動作就完事了,所以邏輯集中在 _pushEntry 中。

在介紹後面的內容之前,我們先介紹一下路由的狀態。

route宣告週期.png

push 和 pop 那一欄主要是開發者呼叫了系統的 API,狀態和方法名一樣,具體的狀態含義如下:

| 狀態名 | 含義 | | ----------- | -------------------------------------------------------- | | add | onGenerateInitialRoute 或者 pages 生成的 Route ,之後會呼叫 install | | adding | 等待頂層路由的結果 | | push | 通過 push 生成的路由,之後會呼叫 install | | pushReplace | 通過 pushReplace 生成的路由,之後會呼叫 install | | pushing | 等待頂層路由的結果 | | replace | 通過 replace 生成的路由,之後會呼叫 install | | idle | 路由已經穩定了,顯示在頁面上 | | pop | 路由要關閉,下一步呼叫 didPop | | remove | 刪除路由,下一步呼叫 didReplace/didRemove | | popping | 等待 finalizeRoute 的呼叫 | | removing | 等待動畫的完成,會移除 overlay 中的頁面內容 | | dispose | 馬上釋放路由 | | disposed | 路由已經釋放掉了 |

我們再看上面的第一處程式碼,

dart _pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push)); // 第一處

因為我們呼叫了 push 的方法,所以構造的 _RouteEntry 的狀態是 push。下面我們看具體的 _pushEntry 邏輯。

```dart void _pushEntry(_RouteEntry entry) { _history.add(entry); //第一處 _flushHistoryUpdates(); //第二處 _afterNavigation(entry.route); //第三處 }

```

我們先看第一處的程式碼,是成員變數 _history 添加了路由包裝類,這裡我們簡單介紹一下 _historyNavigator 2.0 的設計原則就是更新 _history 來實現宣告式的效果,_history 裡面就是存放的已經開啟的路由包裝類,_history 陣列最後一個元素就是當前棧頂的路由或者要新增的路由

我們在看第二處的 _flushHistoryUpdates,它的作用就是重新整理棧頂資料,我們稍後看。

第三處的 _afterNavigation,就是將一些手勢事件取消掉。

所以,從名字可以看出來,邏輯集中在第二處的方法裡。

小結一下:

posh 過程.png

刷新歷史路有棧

下面我們集中精力看 _flushHistoryUpdates

push 程式碼邏輯.png

上面的程式碼基本分為三部分:變數初始化、根據路由狀態呼叫不同的邏輯、顯示路由內容

變數初始化

push 程式碼邏輯 2.png

index :表示當前遍歷到的路由索引,第一個遍歷到的就是棧頂的路由(我們剛新增的)索引。\ entry:表示當前遍歷到的路由,第一個遍歷到的就是棧頂的路由。\ previous:表示 entry 的前一個路由。

我們舉個例子:

頁面route.png

這一 part 主要是變數的賦值,記住它的含義就可以,下面我們看具體的處理邏輯。

根據路由狀態呼叫不同的邏輯

我們呼叫 Navigator.push 的時候,狀態就是 _RouteLifecycle.push,所以就走到了 entry.handlePush 中。我們在看其中的邏輯。 這就是包裝類的作用,原始的路由類中並沒有 handlePush 的方法,而包裝類起到了類增強的效果,和之前的 OverlayEntry 很像。

push 程式碼邏輯 3.png

我們先介紹方法的入參:

| 引數名 | 含義 | | --- | --- | | navigator | 物件 NavigatorState,承載路由的殼子元件 | | previous | 前遍歷到的路由的前一個 | | previousPresent | 當前遍歷到的路由的前一個,和 previous 相比,previousPresent 路由的狀態一定是存在的,而 previous 可能是 remove 的 | | isNewFirst | 是不是要插入到棧頂的路由 |

參考上面的圖:

C 路由是要 push 進來的,AB 是已經在頁面中的。如果 B 的狀態是在 add 和 remove 之間的,比如是 idle 的,那麼 CpreviousPresent 就是 B,否則就是 A

知道了這個我們看其中的邏輯:

```dart void handlePush({ required NavigatorState navigator, required bool isNewFirst, required Route? previous, required Route? previousPresent }) {

final _RouteLifecycle previousState = currentState;

route._navigator = navigator;
route.install(); //第一處

if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
  final TickerFuture routeFuture = route.didPush(); //第二處
  currentState = _RouteLifecycle.pushing;
  routeFuture.whenCompleteOrCancel(() {
    if (currentState == _RouteLifecycle.pushing) {
      currentState = _RouteLifecycle.idle;
      navigator._flushHistoryUpdates();
    }
  });
} else {
  route.didReplace(previous);
  currentState = _RouteLifecycle.idle;
}
if (isNewFirst) {
  route.didChangeNext(null);
}

///... 省略程式碼 } ```

我們看第一處的程式碼,這個是不是很熟悉呀,就是路由的初始化。還記得這個初始化是啥嗎?可以想看前面的線性初始化。 我們的 RouteMaterialPageRoute,就看其中的程式碼。

install.png

做好了顯示的準備工作,我們知道包裝類其實不會做具體的邏輯的,真正執行 push 的邏輯還是在 Route 中,所以就是第二處的程式碼,呼叫了 RoutedidPush。同樣的道理,didPush 也是線性的繼承的,和初始化相比,didPush 簡單的多,我們下面來看:

```dart

Route: TickerFuture didPush() { return TickerFuture.complete()..then((void _) { if (navigator?.widget.requestFocus == true) { navigator!.focusScopeNode.requestFocus(); } }); }

TransitionRoute: @override TickerFuture didPush() { super.didPush(); return _controller!.forward(); }

ModalRoute: @override TickerFuture didPush() { if (_scopeKey.currentState != null && navigator!.widget.requestFocus) { navigator!.focusScopeNode.setFirstFocus(_scopeKey.currentState!.focusScopeNode); } return super.didPush(); }

```

我們看到 didPush 就做了兩件事:焦點控制和動畫驅動。這裡注意一點這個動畫就是初始化時 ModalRoute 中的 buildTransitions 的動畫進度,只要 _controller 的動畫進度變化了,buildTransitions 就會被呼叫。

反過來,我們在看上面的第二處,執行完 didPush 之後,就將 Route 的狀態設定為了 _RouteLifecycle.pushing

這就是包裝類的 handlePush 的指定流程:Route 的初始化、驅動動畫、路由的狀態設定為 pushing。

顯示路由

push 程式碼邏輯 4.png

顯示路由程式碼的很簡單,就是將初初始化的 OverlayEntry 新增到 Overlay 中。

新增的方式和我們之前介紹過的 add 不同Flutter 必知必會系列 —— Navigator 的開始 Overlay,而是重新組織內容。_allRouteOverlayEntries 這個變數儲存的是所有歷史路由的 OverlayEntry,這裡注意一點,每一個路由中含有兩個 OverlayEntry

dart void rearrange(Iterable<OverlayEntry> newEntries, { OverlayEntry? below, OverlayEntry? above }) { final List<OverlayEntry> newEntriesList = newEntries is List<OverlayEntry> ? newEntries : newEntries.toList(growable: false); if (newEntriesList.isEmpty) return; if (listEquals(_entries, newEntriesList)) return; final LinkedHashSet<OverlayEntry> old = LinkedHashSet<OverlayEntry>.from(_entries); for (final OverlayEntry entry in newEntriesList) { entry._overlay ??= this; } setState(() { _entries.clear(); _entries.addAll(newEntriesList); old.removeAll(newEntriesList); _entries.insertAll(_insertionIndex(below, above), old);//第一處 }); } 重點看第一處,就是呼叫了我們之前講過的 insert 流程。走到這裡大家就知道了把,Navigator 的路由管理就是把路由的 OverlayEntry 添加了 NavigatorOverlay 中。動畫是怎麼做的呢?就是在我們的頁面上增加了一個動畫元件來響應動畫驅動器而已,一個路由的顯示層級如下:

頁面層級.png

總結

Navigator 的頁面顯示就是 Overlay 顯示 OverlayEntry,我們自己也可以開發一個簡約版的疊加,不同的是 Navigator 為頁面的顯示增加了過度動畫,焦點控制等。除此之外,不知道大家知道為啥路由中要有兩個 OverlayEntry 不~~~