Flutter 必知必會系列 —— 官方給的 Navigator 2.0 設計原則

語言: CN / TW / HK

Navigator 2.0 的改造真的是大的改造,雖然對以前的程式碼不會產生影響,但是我們可以通過官方給出的設計原則來看改動的範圍。本來打算寫一個總結,我發現這個官方的還是不錯的。

往期精彩

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

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

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

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

Summary

介紹一個宣告式 API 來設定 Navigator 的歷史棧,並且新引入 Router 元件。Router 元件可以根據應用的狀態和系統事件來配置 Navigator

Objective

文章介紹有關 Navigator 的新的 API,可以用宣告式的方式來設定 navigator 的歷史棧,宣告式的關鍵是不可變陣列的 Page。 我們知道 Navigator 管理的是 Route,所以 Page 會轉化成 RoutePageRoute 的關係和 WidgetElement 的關係很像。文件也會介紹現有的命令式的 Navigator API(push,pop,replace等等) 是怎麼重構的,能夠和新的宣告式 API 相互協作。

最後,文章還會介紹一個新引入的元件 —— Router,這個元件可以包裹 Navigator 元件。Router 元件配置一組 Page,然後,Navigator 元件就顯示這一組 Page,當然了新的 Navigator 可以響應來自系統、應用狀態的事件。在程式執行期間,如果接收到新的跳轉等意圖,Router 元件能夠再次配置 Navigator。在點選系統的返回鍵的時候,Router 元件就移除棧頂的路由來實現後退的功能。同時,Router 元件也能夠配置應用的 Navigator 來響應使用者的輸入等等。

Goals

  • 開發者能夠用宣告式的方式來設定和修改 Navigator 的歷史頁面棧
  • 現有的命令式的 API push 等不會受到影響,並且能夠和新 API 相容
  • 開發者將 Navigator 中命令式的歷史堆疊轉為新的宣告式的方式
  • Router 元件可以包裹 Navigator 元件並且可以基於應用狀態和系統事件再次配置路有棧:
  • Router 元件能夠配置 Navigator 展示應用啟動時初始的路由
  • 當收到展示路由的意圖時,Router 能夠再次配置元件 Navigator 以展示頁面。
  • 使用者點選系統返回鍵的時候,Router 能夠再次配置 Navigator 元件,更新頂層的路由實現後退效果。
  • 開發者能夠委託 Router 元件再次配置 Navigator 的歷史路由棧,來顯示新的路由以響應使用者的互動
  • 開發者能夠自定義路由的行為,比如名字與頁面的比對
  • Routers/Navigators 能夠被巢狀,按下系統後退按鈕從最合適的導航器彈出路由

Background & Motivation

這一節討論舊版 Navigator 的能力和缺陷。舊版 Navigator 中,Flutter 提供了兩種方式來配置和修改Navigator 的歷史路由棧:initialRoute 屬性 和 命令式 API (push、pop、pushNamed、pushAndRemoveUntil 等)

Initial Route

initialRoute 引數僅僅只在第一次構建 Navigator 的時候起作用,通常是設定 Window.defaultRouteName,應用啟動的時候就會顯示名字對應的路由頁面。但是,Navigator 構建之後,再次修改 initialRoute 引數是無效的,這種情況下,在程式執行的時候,開發者沒有好的方式來修改,因為開發者不能輕易的替換路由歷史棧。

命令式 API

Navigator 的命令式 API 是 Navigator 中的靜態方法,靜態方法中來獲取 NavigatorState 的例項。這些 API 能夠讓開發者 push 新的路由、移除舊的路由,開發者呼叫這些 API 來響應使用者的互動,比如:使用者點選了 AppBar 的返回按鈕,開發者會呼叫 pop 方法來移除棧頂的路由。當用戶檢視元素的詳情頁,開發者會呼叫 push 方法,來在棧頂新增一個新的詳情頁路由。正如前面所說的,命令式 API 讓每一次路由棧的修改非常具有針對性,而失去了一定的靈活性,比如,開發者不能自由的修改和重新編排路有棧。因此,開發者必須擴充套件現有的 API 來實現某些需求。從本質上來說,這些需求或者靈活性需要的是對歷史路有棧的控制。

在 Flutter 的調研中,也有開發者反饋,命令式的 API 與 Flutter 風格不匹配。在 Flutter 中,如果想要設定 Widget 的子節點顯示,那麼僅僅使用新的子節點來重新構建一下 Widget 就可以了,但是在 Navigator 中不能通過這種方式來實現,必須呼叫笨拙的命令式的 API。

Nested Navigators

舊版 Navigator 的另一個痛點是 Navigator 的巢狀,巢狀 NavigatorsTab 頁面很常見,每個 Tab 下面有一個獨立的 Navigator,然後獨立的 Navigator 巢狀在根 Navigator 下面。巢狀的 Navigator 追蹤歷史路由棧。舊版的 Navigator,Flutter 僅僅只有根 navigator 關聯著系統返回鍵,如果開發者在 Tab 內路由到某個頁面,這可能會造成混亂: 點選系統返回鍵會返回到上一個頁面,而不會返回 Tab 中的歷史棧,同時也汙染了全域性的歷史棧。

Overview

這一節總攬 Navigator 宣告式 API 和新引入的 Router 元件,下一節會詳細介紹實現原理。

Navigator

這一節介紹 Page 的概念,並且將 Navigator 的路由管理分為兩組:Page 的路由和非 Page 的路由。

Pages

為了能夠宣告式的設定 Navigator 的歷史棧,開發者需要通過 Navigator 構造方法來給 Navigator 提供一組 Page 物件。Page 陣列是不變的,並且描述了應該放置在 Navigator 棧中的路由。Navigator 會將一個 Page 物件 inflates 成一個 路由物件。從這方面來看, Page 和 Route 的關係就像是 Widget 和 Element 的關係:Widget 和 Page 僅僅是配置。

Page 物件能夠轉成一個與之相應的 Route 物件,Route 物件可以放在歷史路由棧中。但是並不是每一個 Route 都有一個 Page 與之對應。

開發者可以隨意的實現他們自己的 Page 物件,或者使用 framework 提供的 PageBuilders。framework 使用PageBuilders 來獲取其中的 Route 或者 Widget。特定的 PageBuilder 的會用 Route 包裹與之相適應的 Widget,比如 MaterialPageRoute。

在歷史路由棧中,與 Pages 相對應的 Routes 的順序與提供給 Navigator 的列表中它們對應的Pages 的順序相同。如果設定給 Navigator 的 Page 列表發生了變化,那麼新的列表會與舊的列表進行比較,並且與之對應的歷史路由棧也會更新:

  • 不存在在新列表的路由 Route會從歷史路由中刪除掉
  • 新列表中的 Page ,如果該 Page 還沒有與之匹對的 Route,會首先 inflate 成 Route,然後會被插入到歷史路由棧中指定的位置
  • 歷史路有棧的順序會更新,以確保與新 Pages 列表相同

過渡代理 Transition Delegate 決定了 Route 是怎麼進入退出螢幕的。

對於新增的 Page 引數,Navigator 也提供了一個新的 onPopPage 回撥。Navigator 呼叫這個方法來讓 Page 指定的路由推出。如果接受者同意回撥,框架會呼叫路由的 didPop 方法。如果這個過程成功執行了,框架會用新的 Page 列表更新 Navigator,新的 Page 列表不再包含已經推出的 Page,並且 onPopPage 會返回 true。如果被推出的 Page 沒被移除,它會視為一個新的 Page,新的 Page 會新生成一個 Route。如果 onPopPage 的接收者不想路由被推出,它需要將回調返回 false。onPopPage 回撥僅僅作用於最頂層的 Page。

Pageless Routes

已經存在的命令式 API 通過 push 等方法來插入 Route,這個動作和 Page 沒啥關係。為了不對現有的功能產生破壞性的影響,之前的程式碼還可以繼續執行。在新的 framework 中,Pageless 路由會被繫結到歷史路由棧中 Page 路由的下面。如果,Page 路由在歷史路有棧中的位置變化了,那麼所有的與之繫結的 Pageless 路由也會跟著移動到新位置,並且 Pageless 路由的相對位置不會變化。如果一個 Page 路由從歷史路由棧中移除了,那麼與之繫結的 Pageless 路由也會被刪除。

當 Navigator 第一次插入到元件樹上的時候,負責路由列表初始化的 initialRoute 引數也會生成一個 Pageless 路由列表。初始化的 initialRoute 引數生成的路由會被放置在 Page 引數生成的路由的上面。但是,新框架並不鼓勵使用 initialRoute 引數,在新框架下,最好是提供一個 Page 引數,並且將 initialRoute 設定為 null。

Transition DelegateThe

當 Navigator 中 Page 被新增和移除的時候,那麼過渡代理 transition delegate 決定了與之對應的 Route 應該怎麼進入和退出螢幕。為了做到這一點,過渡代理做了兩個事情:

  1. 當新增/移除路由時,路由是應該動畫的出現/消失還是應該直接出現/消失

  2. 在相同的位置插入和刪除路由的時候,在過渡期間他們應該怎麼排序

我們舉個例子來理解第二個問題,比如 Navigator 的 Page 列表從 [A, B] 變為 [A, C],那麼在過渡上的場景上有兩種可能:

  1. C 被新增(可能帶有動畫)在 B 之上,而 B 被從下面移除(可能帶有動畫)(這個移除可能會延遲到 C 的動畫完成)

  2. C 被新增到 B 下面(可能有一個動畫),並且 B 在它上面移除,以顯示 C

第一個的視覺效果會讓使用者覺得 C 被壓在了 B 上,而 第二個則會讓使用者覺得 B 被彈出來然後顯示 C。

為了實現第一種效果,Navigator 路由棧的順序是 [A, B, C],為了實現第二種效果,路由棧的順序就是 [A, C, B]。 僅僅通過比對新舊 Page 列表,沒有辦法決定哪一種效果是想要的。因此,過渡代理就負責確定 Page 的順序來達到指定的效果。

當 Page 列表改動的時候,過渡代理會收到一組 HistoryDiff 物件,這個物件負責記錄被新增/移除的每一個位置。HistoryDiff 包含了一組排好序的路由(被新增/移除),在這個框架下,可以檢視在 diff 位置之前的歷史堆疊中有哪些 Routes,以及在該位置之後的歷史堆疊中有哪些 Routes。而過渡代理的作用是確定被新增或者移除的 Route 是否應該動畫進來或者動畫出去。更近一步,過渡代理會返回一個新的歷史路有棧來決定想要的順序。路由在新增和刪除列表中的相對順序必須在合併之後的列表中保持。

過渡代理也能決定繫結到已移除路由上的 Pageless 路由應該如何離開螢幕,為了實現這樣的需求,HistoryDiff 包含了一個從 “ Page 路由”到“該路由擁有的 Pageless 路由列表”的對映。只有從列表中刪除的路由可以在對映中存在一個 Entry,因為新新增的路由不能擁有 Pageless 路由。過渡代理不能修改 Pageless 路由的順序,Pageless 路由總是位於其所屬的 Page Route的頂部,它們的生命週期與它的生命週期相關聯。

開發者可以在 Navigator 上配置過渡代理,開發者能夠為每一個 Page 提供不同的代理,這樣可以實現不同的過渡效果。

如果開發者沒有提供自定義的代理,框架會提供一個預設的。預設的效果就是上面講到的第一種方式。對於每一個 HistoryDiff,它會將所有新增的路由放在被刪除路由的頂部,並且當只有歷史路由棧的頂部變化時,才會發生動畫效果:

  • 加入最頂上的路由是被新增的,它會讓路由動畫進來,當動畫完成時,所有其他路由將在沒有動畫的情況下被新增/刪除。

  • 如果路由是被刪除的,那麼在動畫開發之前,其他路由的新增和刪除都不需要動畫

Summary

總之,本文件建議對公共 Navigator API 進行以下更改: - 引入一個新的類 —— Page,它的功能是作為建立路由的藍圖 - 為 Navigator 新增如下的屬性:

List\<Page> 宣告式的設定路由歷史\ onPopPage 讓 Navigator 推出一個 Page\ transitionDelegate 自定義頁面過渡效果

Router

Router 是一個新的元件,起到了分發的作用,來開啟和關閉頁面。它包裹 Navigator 元件,並且根據程式的狀態來配置應該顯示的 Page。更進一步,Router 也會監聽作業系統的事件,並且改變 Navigator 的頁面顯示。

使用 Router 元件的程式可能會管理應用的狀態來顯示內容。不是使用命令式的 API 來顯示新的路由,而是處理程式的狀態。Router 註冊了一個程式狀態的監聽,並且會重新構建 Navigator 來響應狀態的改變。當 Navigator 被用新的 Page 重新配置的時候,會重新生成 Route ,來顯示到螢幕上。Router 的使用過程如下圖所示:

image.png

Router 怎麼獲得程式狀態的改變以及對狀態改變的響應能夠通過 routerDelegate 配置。Router 元件的使用者需要自定義該代理的實現來滿足自己的需求。開發者也可以讓代理來監聽應用狀態的改變,但是這不是必須的。

Router 也可以幫助開發者監聽作業系統相關的事件,Router 支援下面的事件:

  • 程式第一次啟動的初始化 route
  • 監聽作業系統開啟新路由的意圖
  • 監聽作業系統關閉路由棧中最後一個路由的請求

Router 主要是通過 routeNameProvider 代理 和 backButtonDispatcher 代理來監聽事件。 routeNameParser 代理解析命名路由,routeNameParser 會將 routeNameProvider 提供的名字解析成路由資料 T,T 就是路由的範型。框架提供了這些代理的預設實現,一般情況下我們用這些就夠了。預設的代理中,T 就是 RouteSetting 陣列。

routeNameParser 解析的路由資料和 backButtonDispatcher 的返回按鈕通知會被傳遞到 routerDelegate 中,routerDelegate 可能會用新的 Page 來重新 build Navigator。上面的演算法中,routerDelegate 會使用通知來重新配置 app 狀態並且重新構建 Navigator 來響應狀態的改變。

下面的演算法展示了代理的資訊流:

image.png

routerDelegate 與 應用狀態通訊的部分是可選的,取決於開發者提供的 routerDelegate 的具體實現。backButtonDispatcher 和 routeNameProvider 監聽事件的位置和方式(不一定是作業系統)可以通過自定義代理來實現定製。

Route Name Provider

routeNameProvider 代理決定了 Router 如何拿到想要顯示的路由。它是一個 String 的 ValueListenable,並且當 Router 第一次構建的時候,它的值就是 初始化路由。當值改變的時候,Router 就會被通知並且改變 Navigator 的配置,提供給 Navigator 一組新的 Page。

從 ValueListenable 獲得的字串會被 routeNameParser 代理解析成指定的 T,解析之後的資料傳遞給 routerDelegate,這樣可能會重新構建 Navigator。

預設的 routeNameProvider 是 ValueNotifier,包裹 Window.defaultRouteName 作為初始值並且監聽 WidgetsBindingObserver.didPushRoute。 當 didPushRoute 觸發的時候,routeNameProvider 的監聽者就會被通知到。

預設的 routeNameProvider 可以滿足大多數的場景,很少需要自定義。

Route Name Parser

routeNameParser 代理會從 routeNameProvider 中獲取當前路由的字串,並且會將 String 轉成 T。routerDelegate 會用轉化後的資料來配置 Navigator 的路由顯示。

預設的 routeNameParser 會解析 String 為一組 RouteSettings,RouteSettings 代表了 Page,這些 Page 會被push 進入到 navigator 中。預設的代理定義的結構:/foo/bar?id=20&name=mike,這個字串將被解析為三個 RouteSetting 路由/,/foo,和/foo/bar,每個 RouteSetting 將有引數{'id': '20', 'name': 'mike'}與之關聯。

預設的 routeNameParser 是可以滿足大多數場景,開發者幾乎不需要自定義。

Router Delegate

router 代理是整個 Router 的核心,負責構建正確的 Navigator,代理本身是 Router 訂閱的 Listenable,只要代理的資訊進行了改變,Navigator 就會被重新構建。

框架病沒有提供預設的實現為 Router 代理,開發者需要實現自定義的行為來響應狀態和事件。

routerDelegate 也會相應系統事件:

  • 系統返回鍵按下的時候 backButtonDispatcher 會被通知,popRoute 就會被呼叫

  • Router 構建之後就會呼叫 setInitialRoutePath。預設來說,這個方法僅僅呼叫 setNewRoutePath 。

  • 當 routeNameProvider 通知之後 setNewRoutePath 就會被呼叫。具體的名字是 routeNameParser 解析出來的。

Router 不會對 routerDelegate 如何處理這些通知做任何假設。可能的選項包括:

  • 無視這些事件並且不做任何事情

  • 配置應用程式狀態以反映這些通知所請求的更改,然後請求 Router 用重新配置的 Navigator 重新構建

  • 直接用新的 Navigator 來構建 Router

Back Button Dispatcher

backButtonDispatcher 代理會通知 Router 使用者點選了系統的返回鍵,並且會返回到前一個路由。backButtonDispatcher 僅僅作用於有物理返回鍵的。

backButtonDispatcher 是 Listenable 的實現,Router 訂閱了它。當 Navigator 應該 pop 的時候,backButtonDispatcher 會通知它的監聽者。 當然了這種情況僅僅針對物理返回鍵。

dispatcher 可以不通知自己的 Router 監聽器,而是通知一個子節點的 backButtonDispatcher,並讓這個子節點的 Router 監聽器來處理 back 按鈕的按下。這個特性適用於 Tab 巢狀的情況。設計中沒有限制巢狀的級別,子backButtonDispatcher也可以有另一個子節點。

framework 中有兩個 backButtonDispatcher 具體的實現:適用於根 Router 的預設實現、適用於巢狀的實現。

針對根 Router 的預設實現僅僅監聽 WidgetsBindingObserver.didPopRoute ,來決定是否點選了返回鍵。如果有一個的優先順序高於根分發器,它的處理是要麼通知監聽者要麼傳遞給子 backButtonDispatcher。如果多個位元組點都申請了優先順序,那麼最後一個申請的子節點會獲得通知。當子節點決定它不在想要優先順序,那麼它之前的會獲得通知。如果沒有子節點宣告優先順序,那麼通知分發到父節點。

ChildBackButtonDispatcher 本身不會監聽任何系統事件,它僅僅從父節點獲得事件。為了宣告優先順序,它需要持有父節點的引用。為了獲得引用,Routers 被設計為 InheritedWidget。Router 元件持有 backButtonDispatcher 的引用,這個引用就是 ChildBackButtonDispatcher 的父節點。

Example Usage

用新的 Navigator API 來使用 Router,開發者基本只需要實現自定義的 RouterDelegate。對於其他的代理,使用預設的就夠了。這一節就演示 Router 和 Navigator API 是怎麼使用的。

For this example we assume that the stocks app has three screens: 下面的例子中,APP 中有三個頁面: - 主頁展示收藏的股票列表 和 一個搜尋 icon。點選列表進入詳情頁面,點選搜尋 icon 進入搜尋頁面。 - 詳情頁展示關注的股票的細節,頁面中的返回按鈕點選之後返回到前一個頁面。 - 搜尋頁面有搜尋欄、一個返回按鈕和一個搜尋結果。點選結果去詳情頁面。點選返回按鈕去前一個頁面。

程式的狀態是下面這樣的,程式的狀態是一個 ChangeNotifier,可以讓 RouterDelegate 來監聽改變。

```dart class StockAppState extends ChangeNotifier { // If non-null: show the search page with this initial query. String get searchQuery; String _searchQuery; set searchQuery(String value) { if (value == _searchQuery) { return; } _searchQuery = value; notifyListeners(); }

// If non-null: Show the details page for this symbol. String get stockSymbol; String _stockSymbol; set stockSymbol(String value) { if (value == _stockSymbol) { return; } _stockSymbol = value; notifyListeners(); }

// Show these symbols on the home screen. final List favoriteStockSymbols; // Loaded from e.g. a database. } ```

程式的狀態能夠被 RouterDelegate 使用,RouterDelegate 需要傳遞進入 Router。其他的代理可以使用預設的。

```dart class StockAppRouteDelegate extends RouterDelegate> with PopNavigatorRouterDelegateMixin { StockAppRouteDelegate(this.state) { state.addListener(notifyListeners); }

void dispose() { state.removeListener(notifyListeners); }

final StockAppState state;

@override // From PopNavigatorRouterDelegateMixin. final GlobalKey navigatorKey = GlobalKey();

@override void setNewRoutePath(List configuration) { if (configuration.length != 1 || configuration.single.name != '/') { // Don't do anything if the route is invalid. return; } // Update state; if this modifies the state it will call our listener, // which will cause a rebuild. state.searchQuery = configuration.single.arguments['searchQuery']; state.stockSymbol = configuration.single.arguments['stockSymbol']; }

@override Widget build(BuildContext context) { // Return a Navigator with a list of Pages representing the current app // state. return Navigator( key: navigatorKey, onPopPage: _handlePopPage, pages: [ MaterialPageBuilder( key: ValueKey('home'), builder: (context) => HomePageWidget(), ), if (state.searchQuery != null) MaterialPageBuilder( key: ValueKey('search'), builder: (context) => SearchPageWidget(), ), if (state.stockSymbol != null) MaterialPageBuilder( key: ValueKey('details'), builder: (context) => DetailsPageWidget(), ), ], ); }

bool _handlePopPage(Route route, dynamic result) { Page page = route.settings; if (page.key == ValueKey('home')) { assert(!route.willHandlePopInternally); // Do not pop the home page. return false; }

final bool result = route.didPop(result);
assert(result);
// Update state to remove the page in question; if this modifies the state
// it will call our listener, which will cause a rebuild.
if (page.key == ValueKey<String>('search')) {
  state.searchQuery = null;
  return true;
}
if (page.key == ValueKey<String>('details')) {
  state.stockSymbol = null;
  return true;
}
assert(false); // We should never be asked to pop anything else.
return true;

} } ```

Coexistence of imperative and declarative API

正如上面提到的,下面的內容解釋命令式 API 和 宣告式 API。中大型的專案可以選擇通過 Router 來配置 Navigator 的歷史路由棧,並且只使用命令式的 API 來啟動非常短暫的 Route ,比如 Dialog 和 Alert。靈活的 Router 可能在後面更加好用,因為它將更容易整合和支援新的特性,如可連結性和儲存/恢復當前例項狀態(例如,當作業系統由於記憶體不足而在後臺終止應用時)到框架中。

Detailed Design

這一節介紹一些設計上的細節。

Navigator

這一節主要介紹 Page 是怎麼實現的、Navigator 是如何跟蹤路由狀態的、Navigator 怎麼和 Page 陣列同步更新的。

Pages

Route 已經有了一個 RouteSetting 屬性,並且 Page 基本就是 RouteSetting,因為 Page 也可以描述 Route 的配置。因此,將 Page 設計為 RouteSetting 的子類是非常有意義的。

每一個 Page 有一個可選的 Key 屬性,就像 Widget 的 Key 一樣,Key 用來做更新時的標誌位。LocalKey 是非常有用的,因為 Page 僅僅是在一維列表中操作。

為了做到這一點,Page 必須實現 createRoute 方法。createRoute 的入參是 BuildContext,並且返回 Page 相關的 Route。當 Page 第一次新增到 Navigator 的歷史路由棧時,這個方法就會被呼叫。這個方法返回的 Route 必須有 settings 屬性,settings 屬性就是 Page。

最後,Page 也實現了 canUpdate 方法,這個方法就像 Widget 的 canUpdate 方法,預設的實現是當新舊 Page 的型別和 key 相同時,返回true。方法的呼叫時機是給定 Navigator 新的 Page 列表時。

總結一下: ```dart abstract class Page extends RouteSettings { const Page({ this.key, String name, Object arguments, }) : super(name: name, arguments: arguments);

final LocalKey key;

bool canUpdate(Page other) { return other.runtimeType == runtimeType && other.key == key; }

Route createRoute(BuildContext context); }
```

Route State Machine

命令式和宣告式 API 都將 Route 的狀態從一個狀態轉換到下一個狀態,同時會觸發狀態變化的通知。比如,Navigator 被請求彈出一個 Route 時,會呼叫 Route.didPop() 來觸發路由退出過渡,並且等待動畫完成之後釋放 Route。對於 Navigator,Pop 請求會將 Route 的狀態從 idle 過渡到 "waiting for pop to complete" 在到 disposed。目前這些生命週期狀態更改用命令式API隱式編碼。

命令式 API 必須實現和宣告式 API 一樣的過渡。為了避免重複編碼,生命週期的過渡提取到了共享方法中,並且在呼叫核心方法之前,兩者的 API 僅僅標記路由的過渡。核心的方法就是 flushHistoryUpdates,它會執行真正的過渡,觸發所有的過渡效果。

下面的演算法描述了 Route 的生命週期轉換。用*標記的狀態是瞬間的,並且是標記狀態,告訴 flushHistoryUpdates 路由下一步需要執行什麼過渡。#標記的表明 Route的停留態,會一直停留到下一個事件。

image.png

下面的事件決定了 Route 的生命週期:

  • 解析 initial 引數生成的 Route,被新增到歷史路由棧中,是 add*狀態

  • push() 方法新增的 Route 是從 push* 狀態開始,被 pushAndRemoveUntil() 方法移除的路由是 remove* 狀態

  • pushReplacement() 方法新增的 Route 是從 pushReplace 開始的,並且被替換之後,會是 remove 狀態

  • replace() 方法新增的路由從 replace 狀態開始,並且被替換時是 remove

  • pop() 方法會讓頂層的 Route 成為 pop* 狀態

  • 通過 Page 陣列新增到 Navigator 中的路由,可能會從 push、 replace、 或者 add* 狀態 開始,具體是那一個則由過渡代理決定

  • 通過 Page 陣列移除 Navigator 的路由,可能會是 pop、 complete、 或者 remove*,具體是那一個則由過渡代理決定

  • push 動畫完成之後,就從 pushing# 狀態開始往下走

  • finalizeRoute 被呼叫之後,表明 pop 動畫已經完成了,就從 popping# 繼續往下走

  • 當 Route 的所有 push 完成之後,或者頂上的 Route 是 idle 的,路線就會從 removing# 和 adding# 繼續向下走,以避免可見的視覺故障。

通知 Route 它們新的上一個/下一個路由(通過didChangeNext()或didChangePrevious())被延遲,直到所有由星號指示的瞬態變化被處理。這樣可以避免將即將消失的瞬態狀態通知給 Route。Route 也只被告知下一個活躍的 Route 。

圖中沒有顯示的是在 push 動畫仍在執行時移除 Route 的邊緣情況(它處於push #狀態)。這種情況也是完全支援的 : push# 中的路由可以跳過 idle 狀態,直接在 idle 後進入其中一種狀態。

Route 物件和它的狀態會被繫結到 RouteEntry 物件中。Navigator 的歷史路由棧僅僅是 RouteEntries 陣列。

Updating Pages

當 Page 陣列被改變的時候,歷史路由棧就會被更新。對於這種情況,Navigator 構造一個新的與 Page 陣列向匹對的歷史路由棧,如果 canUpdate 返回了 true ,表明新 Page 與舊 Route 可以匹對。預設,比對是 runtime 型別和 key。如果 Page 皮對了 Route,Route 的 RouteSetting 就會用新的 Page 更新。Route 的 RouteEntry 和 Pageless Route 就會被複制到新的歷史路由棧中

如果舊列表中沒有匹對的 Page,那麼 Page 就會呼叫 createRoute。新生成的 Route 就會用 RouteEntry 包裹,並將其新增到新的歷史路由棧中

這個過程會遍歷整個 Page 陣列。最後,沒有匹對 Page 的 Route 會被標記為 removed ,並且也會被複用當有新的 Page 列表中包含的時候。歷史路由棧中保持的 Route 可以被觸發,並且等待釋放

idle狀態之後的路由不能再與 Page 匹配,因為它們基本上是在退出

過渡代理決定被新增和被刪除的 Route 的順序,也會決定 Route 如何過渡進入和過渡退出

Transition Delegate

從機制的角度來看,過渡代理決定被新增到歷史路由棧中的 Route 是什麼狀態的。上面的狀態圖有:push、replace 和 add。相似的,idle 狀態之後的狀態應該是啥,也是過渡代理決定的。idle 狀態之後的狀態是:pop、 remove、 and complete

上面提到了,合併之後的歷史路由棧被分割為多個 HistoryDiff,這些 HistoryDiff 會逐個交給過渡代理。下面的例子展示了差異是如何建立的,小寫字母表示 Pageless 路由,大寫字母表示 Page 路由,PA 表示路由 a對應的頁:

image.png

會產生兩個 HistoryDiffs,第一個包含下面的資訊:

image.png

過渡代理接受這些 diff,會呼叫 E 和 F 的方法,將它們標記為 push、 replace、或者 add 之一的狀態。相似的,B 、C ,以及與之關聯的 Pageless 路由 x、 y、 z 也會被呼叫 pop、remove、 and complete 中的一個方法。最後,返回一個新的列表,新的列表合併了新增和刪除的路由,結果就是 [E, B, F, C]。第二個 HistoryDiff 就是下面的:

企業微信截圖_a25e3968-75ec-4916-9cc2-3340d19fa6d8.png

過渡代理處理 Diff 的方式類似,因為 "Routes after diff" 是空的,過渡代理能夠推斷 HistoryDiff 是歷史有棧的頂部。在 HistoryDiff 中新增和刪除列表以及對映中包含的 Route,基本上是 RouteEntries 的只讀檢視,Navigato 在內部使用它來管理歷史堆疊。 只讀的檢視能夠讓過渡代理訪問 Route 的狀態和 RouteSettings。也暴露了一些方法來讓改動 RouteEntry 的狀態。其他的 RouteEntries 不能夠訪問其他的額外方法。

Routes & RouteSettings

現在,如果要想無動畫的新增一個 Route 進入到歷史路由棧,方式就是把它作為一個初始化路由。而宣告式 API 實現起來就很簡單了。尤其是路由被遮擋的時候,這種場景比較有意義。為了支援這一點,didAdd 方法就被添加了。和 didPush 相比,didAdd 方法是無動畫的,這個方法也會用於初始化路由。initialRoute 就是已經無用的了,這一個改變。

Router

Router 和它的各種委託代理之間的 API 是完全非同步的。當新的 Route 是可用的時候,routeNameProvider 會非同步的通知 Router。routeNameParser 會返回一個 Future ,這個 Future 在解析完成的時候就會生成一個結果。當新的 navigator 配置是可用的時候,routerDelegate 就會通知 Router。

非同步的設計讓開發者有了更高的靈活性:routeNameParser 可能需要與OEM執行緒通訊以獲取更多的 Route 資料,通訊是非同步的,因此解析的 API 也需要是非同步的。相似的,設計也作用於其他的 API 方法。

當代理可以完全同步地執行它們的工作時,那麼在它們的實現中應該使用 SynchronousFuture。這將允許 Router 以完全同步的方式進行。然而,如果需要的話,Router 完全支援非同步工作。

要啟用正確的非同步處理,在實現 Router 時必須特別注意 : 當代理返回的 Future 完成時,需要檢查建立 Future 的請求是否仍然是當前請求。如果另一個新的請求已經被髮出,那麼 Future 的完成值應該被忽略。

Router Delegate

routerDelegate 必須實現下面的介面:

dart abstract class RouterDelegate<T> implements Listenable {   void setInitialRoutePath(T configuration);  void setNewRoutePath(T configuration);   Future<bool> popRoute();   Widget build(BuildContext context); } RouterDelegate 的任務是返回一個配置正確的 Navigator,Navigator 的配置是 Page 陣列,這個陣列是應該展示到螢幕上的。只要 RouterDelegate 改變了 Navigator 的配置,那麼應該呼叫 notifyListeners() 方法。Router 元件會根據這個訊號進行重新構建並且請求一個新的 Navigator 從 RouteDelegate 的 build 方法中。

routerDelegate 的其他方法會被 Router 呼叫,這些方法用來響應系統事件:當初始化 Route 或者解析 routeNameProvider 的新 Route,那麼 setInitialRoutePath 和 setNewRoutePath 就會被呼叫。為了響應這些呼叫,代理就會通過 notifyListeners 進行通過,Navigator 就會重新構建來響應通知。

當 backButtonDispatcher 報告作業系統想要返回的時候, popRoute() 就會被呼叫。這可能會導致 RouterDelegate 將 pop 轉發給之前的 Navigator,如果 RouterDelegate 能夠處理這個 pop,那麼就返回 true,否則返回 false。返回 false 之後的行為就取決 於 backButtonDispatcher 的實現

Back Button Dispatcher

As described in the Overview section, the framework will ship with two concrete BackButtonDispatcher implementations (RootBackButtonDispatcher and ChildBackButtonDispatcher), who both implement the following interface: 就像前面描述的,框架提供了兩個具體的 BackButtonDispatcher 實現,BackButtonDispatcher 的介面如下:

dart abstract class BackButtonDispatcher implements Listenable {   void takePriority();   void deferTo(ChildBackButtonDispatcher child);   void forget(ChildBackButtonDispatcher child); }

當在 ChildBackButtonDispatcher 上呼叫 takpriority() 時,它會在它的父類上呼叫 deferTo()。父節點記住順序列表中所有呼叫該方法的子節點。當父節點(或作業系統的RootBackButtonDispatcher)通知它已經按下了後退按鈕時,它會通過方法呼叫將此通知轉發給該列表中的最後一個子節點。如果列表為空,它通過呼叫父類中的 notifyListeners() 來通知它的 Router。如果子程式不再想接收返回按鈕通知,它也可以在父程式上呼叫 forget()。在這種情況下,父從它的內部列表中移除子。

當在任何 BackButtonDispatcher 上呼叫 takpriority() 時,dispatcher 也將清除它的內部子列表,不再將後退按鈕通知轉發給任何子節點。

Integration

目前,Navigator 已經被整合入了 WidgetsApp 元件中,整合的原因是希望使用者能夠受益,我們希望 Router 能夠成為 Flutter 應用中最佳的互動方式

雖然現在命令式和響應式的 API 都並存著,框架層為 MaterialApp 新增加了一個命名構造 withRouter,新的構造方法可以設定上面介紹過的代理。

新的構造方法不能設定下面的引數,這些功能可以在代理中實現:

  • navigatorKey
  • initialRoute
  • onGenerateRoute
  • onUnknownRoute
  • navigatorObservers
  • pageRouteBuilder
  • routes