在開始以前,咱們先介紹一個貌似絕不相關的概念,至於緣由,一是由於後面的某個概念和它具備相關性,二是由於這個概念太簡單,不足以以一整篇篇幅來介紹它,因此不如就在這裏順帶着介紹一下。api
一句話總結 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
這個從視圖樹上按名稱摘果子的過程並不難理解。好了,關於 InheritedWidget
的部分咱們就瞭解這麼多,下面迴歸本篇的核心主旨——路由部分。函數
Navigator
以棧的方式管理着它的家族控件們。正如在 Android 中經過一個棧來管理 Activity
,每一個 Activity
做爲一個單獨的頁面的原則, Flutter 中也以棧的方式管理着咱們須要的頁面,不過每一個頁面再也不是 Activity
,而變成了 route。oop
說到 Navigator
,咱們能夠在腦海中造成這樣一種畫像,在桌子上摞着一疊圖紙,咱們能看到最上面的那一張畫了些什麼,可是沒法其餘在下面的圖紙的內容。若是如今把最上面的那張圖紙拿開,原先自上而下的第二張此刻就變成了最上面的那張圖紙,此時咱們看到畫像就仍是新的最上面的那幅。那再放置一張新的畫像在這一摞圖畫之上,可見的圖畫就又被更新了。post
當咱們須要添加新的「圖畫」時,只須要使用 Navigator
的 push
系列方法就能夠了,push
系列方法有好些個,忽略其餘的附加操做,它們能夠分爲兩類—— push
和 pushNamed
。學習
push
Navigator.of(context).push(MaterialPageRoute(builder: (context) => SignUpPage()));
複製代碼
這句代碼並不特別,是跳轉一個新界面的通常作法。MaterialPageRoute
是 Route
的子類,builder
參數返回新界面的 Widget
實例。ui
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
中,這樣咱們的界面就能夠顯示在頁面層級中了。
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。
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
到目前爲止,一切都運行良好,可是它的侷限也很明顯。首先,它沒法一次性壓入多個頁面;其次,它只能彈出最上層的頁面,對於某些場景下彈出下層頁面的需求則沒法知足。因此 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
對象。
Page
Page
是一個頁面的抽象,繼承自 RouteSettings
類,經過 name
屬性來標識頁面。正如 Widget
到 Element
經過 createElement()
方法,Page
中也有一個方法 createRoute()
用來建立 Route
實例。
經過上面 Navigator 1.0 的分析,咱們知道 Route
是 Flutter 路由進行頁面切換的載體,包裹着真正的頁面在棧中「騰挪閃轉」,從而實現頁面切換的功能。
RouterInformationProvider
這個類經過它的 value
屬性傳遞值給 RouteInformationParser
類的 parseRouteInformation
方法,該值即 RouteInformation
對象,儲存路由的地址,經過該地址能夠控制頁面跳轉。
例如當咱們在瀏覽器的地址欄輸入 「/index」後綴做爲新的跳轉地址後,RouteInformationParser
類的 parseRouteInformation
方法便可接收到 location
屬性存儲有 「/index」值的 RouteInformation
對象。
RouteInformationParser
該類提供了兩個方法,分別是 parseRouteInformation
和 restoreRouteInformation
。
parseRouteInformation
方法接收地址信息—— RouteInformation
,而後返回 Future<T>
類型對象,「T」是一個約定的任意類型,返回的 Future<T>
類型將在 RouterDelegate
類的 setNewRoutePath
方法被接收,能夠在該方法中真正實現頁面添加跳轉的邏輯。
一般該方法的調用來自瀏覽器地址欄輸入地址後跳轉,而咱們經過 navigator 實現的界面跳轉不會致使該方法被調用。
restoreRouteInformation
方法用來恢復瀏覽歷史頁面,好比咱們須要作「前進」或「後退」的功能而保持瀏覽器地址欄中的地址不變,則能夠經過 Router
類的 navigate()
方法強制上報路由信息從而觸發該方法。該方法返回的 RouteInformation
對象被 parseRouteInformation
方法接收和處理。
RouterDelegate
該類是處理路由地址的主要類,頁面的壓入與彈出都在這個類中進行。
首先,這個類經過 setNewRoutePath
方法接收新的路由地址,而後對新的地址進行查找(通常在用戶本身維護的路由表中),將對應的頁面壓入棧。其次,該類提供了 build
方法,Router
對象會調用該方法獲取視圖樹對象,因此該方法中應當返回能表明當前視圖樹的 Widget
對象,以供系統對顯示視圖進行更新。
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 便可。