在開始之前,我們先介紹一個貌似毫不相關的概念,至於原因,一是因為後面的某個概念和它具有相關性,二是因為這個概念太簡單,不足以以一整篇篇幅來介紹它,所以不如就在這裡順帶著介紹一下。
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 的資料並未改變時,可能並不需要對其進行更新。
所以,相比於一般的 widget,它主要多了個在檢視樹上實現「資訊傳遞」的功能,那它的資訊傳遞的功能又是如何實現的呢——藉助 BuildContext
類,我們線看一個例子。
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
類中。
這個從檢視樹上按名稱摘果子的過程並不難理解。好了,關於 InheritedWidget
的部分我們就瞭解這麼多,下面迴歸本篇的核心主旨——路由部分。
從 Navigator 1.0 開始
Navigator
以棧的方式管理著它的家族控制元件們。正如在 Android 中通過一個棧來管理 Activity
,每個 Activity
作為一個單獨的頁面的原則, Flutter 中也以棧的方式管理著我們需要的頁面,不過每個頁面不再是 Activity
,而變成了 route。
說到 Navigator
,我們可以在腦海中形成這樣一種畫像,在桌子上摞著一疊圖紙,我們能看到最上面的那一張畫了些什麼,但是無法其他在下面的圖紙的內容。如果現在把最上面的那張圖紙拿開,原先自上而下的第二張此刻就變成了最上面的那張圖紙,此時我們看到畫像就還是新的最上面的那幅。那再放置一張新的畫像在這一摞圖畫之上,可見的圖畫就又被更新了。
當我們需要新增新的「圖畫」時,只需要使用 Navigator
的 push
系列方法就可以了,push
系列方法有好些個,忽略其他的附加操作,它們可以分為兩類—— push
和 pushNamed
。
1.push
Navigator.of(context).push(MaterialPageRoute(builder: (context) => SignUpPage()));
複製程式碼
這句程式碼並不特別,是跳轉一個新介面的一般做法。MaterialPageRoute
是 Route
的子類,builder
引數返回新介面的 Widget
例項。
Navigator
是 StatefulWidget
的子類,對應的 State
為 NavigatorState
。Navigator.of(context)
方法返回 NavigatorState
例項,和上面 InheritedWidget
類似,藉助 BuildContext
的 findRootAncestorStateOfType()
方法在 Element 樹上尋找對應的 StatefulElement
,返回可以和泛型指定的 State
型別匹配的 State
物件。
在深入 push()
方法之前,我們先借助 devtools 瞭解一下 Flutter 頁面的層級結構,能夠幫助我們更好地理解下面的流程。
由於圖片太長,請點選檢視
接下來就是呼叫 Navigator
的 push()
方法了,這部分的邏輯比較複雜,我嘗試著按我的理解繪製了一張流程圖,對照著來理解整個過程。
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()
方法呼叫 _RouteEntry
的 handlePush()
方法,建立 「_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 name
和 final Object arguments
。而 Navigator
和 NavigatorState
的 pushNamed()
方法引數接收的正是這兩個物件。
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
變數控制彈出的過程,分別為pop
、poping
、remove
、removing
、dispose
、disposed
,並在這些過程中移除 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
類的例項,而 _popCompleter
的 future
屬性在 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
一、用法介紹
Navigator
到目前為止,一切都執行良好,但是它的侷限也很明顯。首先,它無法一次性壓入多個頁面;其次,它只能彈出最上層的頁面,對於某些場景下彈出下層頁面的需求則無法滿足。所以 Navigator 2.0 就應運而生了。
在 Flutter 迭代到 1.22 版本後,關於 Navigator 的部分新增了一些新的 api:
Page
—— 抽象的「頁面」的概念,對Route
配置選項的一種描述;Router
—— 管家角色,應用中頁面開啟或關閉的排程員,監聽來自於系統的路由資訊(如啟動路由、新路由加入或者系統返回按鈕的訊息等);RouteInformationProvider
—— 更改路由獲取到的頁面的名字;RouteInfomationParser
—— 接收來自RouteInfomationProvider
的RouteInfomation
並將其轉化為泛型約束的資料型別;RouterDelegate
—— 輸入來自RouteInformationParser
的資料,負責將提供的 navigator 頁面插入檢視樹,同時接受監聽更新檢視;BackButtonDispatcher
—— 監聽返回按鈕事件。
Navigator 2.0 的概念和之前介紹過的 Flutter 檢視樹比較相似——Widget
儲存著檢視的配置,通過 Widget
物件建立對應的 Element
和 RenderObject
——Page
物件是關於路由的配置的抽象的概念,而通過它的 createRoute()
方法建立 Route
物件。
1. Page
Page
是一個頁面的抽象,繼承自 RouteSettings
類,通過 name
屬性來標識頁面。正如 Widget
到 Element
通過 createElement()
方法,Page
中也有一個方法 createRoute()
用來建立 Route
例項。
通過上面 Navigator 1.0 的分析,我們知道 Route
是 Flutter 路由進行頁面切換的載體,包裹著真正的頁面在棧中「騰挪閃轉」,從而實現頁面切換的功能。
2. RouterInformationProvider
這個類通過它的 value
屬性傳遞值給 RouteInformationParser
類的 parseRouteInformation
方法,該值即 RouteInformation
物件,儲存路由的地址,通過該地址可以控制頁面跳轉。
例如當我們在瀏覽器的位址列輸入 「/index」字尾作為新的跳轉地址後,RouteInformationParser
類的 parseRouteInformation
方法即可接收到 location
屬性儲存有 「/index」值的 RouteInformation
物件。
3. RouteInformationParser
該類提供了兩個方法,分別是 parseRouteInformation
和 restoreRouteInformation
。
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 注。下面簡單串一下系統的執行流程。
二、原理分析
首先 MaterialApp.router()
構造方法會傳入 routeInformationParser
和 routerDelegate
等物件,_MateiralAppState
物件在 build()
方法中呼叫 _buildWidgetApp()
方法構造 WidgetsApp
物件,因為 routerDelegate
物件是必填欄位,所以 bool get _usesRouter => widget.routerDelegate != null;
欄位為 true
,會通過 WidgetsApp.router
建構函式構造,然後在 _WidgetsAppState
類的 build()
方法中構造 Router
物件,所以它的層級結構如下(當然,它們之間還穿插著其他的包裝類):
Router
類繼承自 StatefulWidget
,那麼老規矩,還是看 _RouterState
的 build()
方法:
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,
),
);
}
複製程式碼
可見,最終還是會呼叫 RouterDelegate
的 build()
方法來建立頁面,該方法由開發者實現。
我們對該方法的實現如下:
@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
都很熟悉了,但是這裡的用法又和上面介紹的兩種用法都不一樣。
這裡通過 Navigator
的 pages
屬性,將頁面列表 List<Page>
傳遞進去,當檢視配置有變更時,觸發檢視更新,此方法被呼叫,然後通過比較 pages
是否已產生變化,來決定是否更新頁面,最終會呼叫 Navigator
的 _updatePages
方法。這個方法的內容有點多,我們就不做具體說明了,只大概說一下它的工作流程。
這個方法比較新的 pages
列表和舊的 _history
列表(元素為 _RouteEntry
型別),然後產生新的 _history
列表。這個方法大致和 RenderObjectElement.updateChildren()
方法流程相同。
需要注意的是,這個方法全程在圍繞著兩個列表進行——舊的路由列表 _history
以及新的頁面列表 widget.pages
,我們把前者稱為「oldEntries」,把後者稱為 「newPages」,通過兩個列表共同比對,剔除 oldEntries 中非 Page
型的節點,而用 newPages 中的節點更新對應的 oldEntries 的節點。
-
首先從 List 頭開始同步節點,並記錄非
Page
的路由,直到匹配完所有的節點。 -
從 List 尾部開始遍歷,但不同步節點,直到不再有匹配的節點,然後最後同步所有的節點,之所以這麼做,是因為我們想以從頭到尾的順序來同步這些節點。此時,我們將舊 List 和新 List 縮小到節點不再匹配的位置。
-
遍歷舊列表被收縮的部分,獲得一個儲存
Key
值的 List。 -
正向遍歷新 List 被收縮的部分(即去除已遍歷兩端的中間部分):
- 對無
Key
元素建立_RouteEntry
物件並將其記錄為transitionDelegate
(轉場頁面); - 同步有
Key
的元素列表(如果存在的話)。
- 對無
-
再次遍歷舊 List 被收縮的部分,並記錄
_RouteEntry
和非Page
路由(需要從transitionDelegate
中被移除)。 -
從列表尾部再次遍歷,同步節點狀態,並記錄非
Page
頁面。 -
根據
transitionDelegate
配置轉場效果。 -
將非
Page
路由重新填充回新的_history
。
更新過 _history
之後,剩下的流程就和 Navigator 1.0 中介紹的相同了——通過 Overlay
物件更新頁面棧,完成頁面顯示和切換的需求。
Navigator 2.0 的思路就是將頁面的排列和更替通過一個 Page
列表—— pages
完全交給開發者,開發者只需要維護好 pages
,轉化為真正可顯示的介面的過程就交給 Flutter engine 即可。