前言
這一次,我嘗試以不貼一行原始碼的方式向你介紹 Flutter 路由的實現原理,同時為了提高你閱讀原始碼的積極性,除了原理介紹以外,又補充了兩個新的模組:從原始碼中學習到的程式設計技巧,以及 閱讀原始碼之後對實際應用開發帶來的幫助。
希望這樣1+2的模式,可以誘導你以非常積極的心態,很輕鬆的學習到 Flutter 路由相關的知識。
當然,不貼一行原始碼,純白話的講解必然會隱藏很多細節知識,渴望知識的你一定不會滿足的,所以我在原理介紹結束之後,又補充了一篇番外:貼了大段大段的純原始碼解析的《Flutter 路由原始碼解析》,知道大概原理的你再去讀原始碼,一定會輕鬆很多吧,希望這樣的模式可以有效的幫助到你!
本文大綱:
-
Flutter 路由實現
- Flutter 路由實現的底層依賴
- Flutter 路由過程
- 番外:《Flutter 路由原始碼解析》
-
Navigator 原始碼裡非常值得學習的 Flutter 程式設計技巧
- Flutter中的“指令式程式設計”:物件引用查詢與遠端區域性更新
- 元素自治
- 私有類包裝:隔離邏輯與Widget
-
閱讀 Navigator 原始碼之後對實際應用開發的幫助
- 路由動態監聽
- 路由監聽中,識別 彈窗 or Page
- 動態新增 Widget
- 易踩的坑:多Navigator巢狀情況下的錯誤路由查詢
Flutter 路由實現
Flutter 路由實現的底層依賴
如果你對 Flutter 的Widget Tree有些瞭解。應該知道 Flutter 中的根 Widget 是RenderObjectToWidgetAdapter
,根 Widget 的 child 就是我們在void runApp(Widget app)
中傳入的自定義 Widget。
從runApp
開始,Flutter的列車便轟隆隆開動了。似乎一切都順理成章,直到我們開始思考,執行Navigator.push()
方法開啟新的頁面是如何實現的?
再繼續分析之前,我們不妨先自己想想:如果讓你來設計,你會如何設計Navigator
? 如果是我的話,我大概會這樣設計, 然後把這個 MyNavigator
放到 Widget 樹的根部:
// 虛擬碼
class MyNavigator extends StatefulWidget{
...
Map<String,Page> pageMap = ...;
String currentPage = "one";
void setCurrentPage(String pageName){
setState(() {
currentPage = pageName;
});
}
@override
Widget build(BuildContext context) {
return pageMap[currentPage];
}
}
複製程式碼
那Flutter是如何實現的呢?
我們先看一個最普通的Flutter App 的 Widget 樹結構:
哈哈,這個圖乍一眼看有點懵,陌生的 Widget 可能有點多,挨個簡單解釋一下:
-
RenderObjectToWidgetAdapter: Flutter 中的 root widget。
-
MyApp: 我們在
void runApp(Widget app)
中傳入的自定義 Widget。 -
MaterialApp : 就是那個Flutter 官方的
MaterialApp
元件,通常我們會在自定義的根佈局使用它,不用它的話很多官方Widget就沒辦法使用了。 -
Navigator: 實現路由跳轉用的就是它,它也是一個Widget,已經早早的嵌入了 WidgetTree 中。它維護了一個
Route
集合,你呼叫push,pop方法時,Navigator
都會以棧的方式對這個集合進行新增或刪除,就是你所熟悉的介面棧。 (和我們所想的方案几乎一樣是不是?) -
Overlay: 顧名思義,一個可以實現一層層向上覆蓋 Widget 的元件,它持有了一個集合
List<OverlayEntry>
,你可以獲取這個 Widget 的引用來向集合中新增元件,使得新的元件覆蓋在已有的元件上面。在Navigator
體系中,你的Route
物件,都將轉化為OverlayEntry
物件存入這個集合。 -
OverlayEntry:
OverlayEntry
在圖中沒有,因為它不是一個 Widget,而是一個純純的 Dart 類。Overlay
維護的是一個純 Dart 類集合,最後再根據這個 Dart 類集合去建立 Widget。為什麼這樣做呢?因為直接操作Widget並不是一件優雅安全的事情,這一點我們再下一節會將。 -
_Theatre: 這是一個私有類,並且只有Overlay使用了它。它的名字非常形象的表達了它的功能:劇院。你有很多元件以一層層覆蓋的模式繪製在介面上,如果其中某一層的元件以全屏不透明的模式繪製在介面上,那它下層的元件就不需要再進行繪製了(下面的完全看不到了還繪製啥呀~)。
_Theatre
就在做這樣的事,需要繪製的元件放置在“舞臺之上”,其他不需要繪製的元件作為觀眾放置臺下,僅 build 但不繪製。 -
Stack: 類似 Android 裡的
FrameLayout
, 順序繪製它的 child,第一個child被繪製在最底端,後面的依次在前一個child的上面。 -
_OverlayEntry: 上面我們有提到
OverlayEntry
,這個純 Dart 類最終會以_OverlayEntry
的形式進入 Widget 樹中。Overlay
是以_OverlayEntry
為最小 Widget 單位向Stack
中新增 child 的。_OverlayEntry
包裹了我們的自定義 Page。 -
MyPage:就是你自定義的頁面。
聽起來Overlay
和Stack
功能完全一樣?
瞭解這個之前,你需要知道每個通過路由展示在介面的上的 Page 或 PopupWindow,都有兩個狀態值:
- opaque:是否是全屏不透明的,當一個元件
opaque=true
時,那麼可以認為它之下的元件都不需要再繪製了。一般情況下,一個 Page 的opaque=true
, 不是全屏的 PopupWindowopaque=false
。 - maintainState:一個已經不可見(被上面的蓋住完全看不到啦~)的元件,是否還需要儲存狀態。當
maintainState=true
時,會讓這個元件繼續活著,但不會再進行繪製操作了。如果maintainState=false
,則不會繼續維持這個 Widget 的生存狀態了。
畫個圖來解釋下,線框代表螢幕,長條代表一個個層疊繪製在螢幕上的元件。藍色代表需要被繪製,黃色代表需要維持它活著但不需要繪製,灰色代表可以被拋棄的。
好了,知道這兩個知識點以後,我們繼續講Overlay
和Stack
。
Overlay 的能力趨向於是一種邏輯管理,它維護了所有準備要繪製到介面上的元件。並倒序(後加入的元件繪製到最上方)遍歷這些元件,最開始每個元件都有成為“演員的機會”,直到某個元件的opaque
的值為 true , 後面的元件便只有做 “觀眾” 的機會了,對於剩下的元件,判斷他們的maintainState
值,為 true 的才有機會做觀眾,為false的沒有入場機會了,他們這一階段的生命週期將結束。之後將分類完成的“演員”和“觀眾”交給 _Theatre。
_Theatre
維護了兩個屬性:onstage
和offstage
,onstage
裡又持有了一個Stack
,將所有的“演員”加入Stack
中,依次覆蓋繪製。offstage
維護了所有的“觀眾”,只build
他們,但不繪製。
Overlay
管理OverlayEntry
的邏輯類似下面這張圖:
可能你會比較好奇_Theatre
中 offstage
是如何做到不繪製的。
你應該知道 Element 樹的是通過其內部的mout
方法將自己掛載到父 Element 上的。_Theatre
的 mout
方法不太一樣, onstage
走正常的掛載流程,加入到Element
樹中; offstage
集合中的 Widget 僅建立了對應的 Element
,並沒有掛載到 Element
樹中。沒有進入到Element
中,也就不會進入到 RenderObject
樹中,也就不走到繪製流程了。
這樣你應該能理解Overlay
其實是Stack
的擴充套件。Overlay
預先進行過濾,從而避免了無用的繪製。
Flutter 路由過程
經過上面講的 Widget 樹結構以及Overlay
的能力和原理,你大概能猜到Navigator
就是在Overlay
的基礎上擴充套件實現的。那具體是怎樣一個過程呢?
你已經知道Navigator
維護了一個 Route
集合(就是一個很普通的 List)。你呼叫push
方法向集合中增加一個 Route
時, 也同時會建立出兩個對應的OverlayEntry
, 一個是遮罩(對原理解釋並不是很重要,後面我們會忽略它,你只要知道就好了),一個代表 Page 本身。
我們看下當路由中只有一個 Page 時的示意圖:
Navigator
持有一個 Route
集合,裡面只有一個 Page。同樣的,Navigator
內部的Overlay
也持有一個OverlayEntry
集合,並且有與 Page 對應的OverlayEntry
。需要提醒的是,Route
與OverlayEntry
並不是一一對應的,而是1:2,上面我們講了還有一個遮罩,只是這裡為了圖示簡單,省略了它。
因為只有一個 Page 需要展示,所以它在_Theatre
的onstage
的Stack
裡,最終將被繪製。此時offstage
為空。
我們再看下當路由中又被 push 進一個 Page時的情況:
因為通常 Page 的 opaque=true, maintainState=true
,所以 Page2 進入 onstage
, Page1 不在需要被繪製,但需要保持狀態,進入了offstage
。
當我們再次向路由中 push 一個 Page:
我們已經看了3個 Page 的情況,再看一個 popupWindow(dialog) 的情況,因為通常 popupWindow(dialog) 的 opaque=false
,我們再向路由中 push 一個 dialog:
因為 dialog 並不是全屏不透明的,它下面還是要展示Page的一部分,所以它要和 Page 一起繪製在螢幕上,只不過 dialog 在最上層。
pop 的過程就是 push的反向,把4張圖倒著看一遍就ok啦。
這裡只講了最簡單的 push ,Navigator
還提供了豐富的push和pop方法,但最終只是最基礎的push或pop的擴充套件。
比如pushNamed
,其實就是通過字串匹配建立出對應的Route
,然後呼叫push
方法;pushReplacement
其實就是push的同時pop
出舊的Route
,以你的聰明才智,一定很輕易就能猜到實現邏輯的,這裡我就不多介紹啦。
番外:《Flutter 路由原始碼解析》
求知慾如此之強的你,一定渴望更多的細節。如果你還精力旺盛,就繼續跟我去看看原始碼吧~
不閱讀原始碼不影響接下來的閱讀哦~,但如果讀過之後,下面的內容會更香!
Navigator 原始碼裡非常值得學習的Flutter 程式設計技巧
Navigator 體系的原始碼,閱讀理解起來,不是特別難,但也有一定的複雜性。所以其中包含了一些非常值得學習的 Flutter風格程式設計技巧。
Flutter中的“指令式程式設計”:物件引用查詢與遠端區域性更新
區別於Androdi&iOS的指令式程式設計正規化,Flutter 宣告式的程式設計正規化早期會給開發者帶來極大的不適。最大的改變在於UI的更新是通過 rebuild 來實現的,以及物件引用的概念被弱化了(如果每一次都是重新建立,那持有一個 Widget 的引用也就不是很重要了)。
這樣的改變較為容易引起你不適的點在於:
-
1.在 Widget 樹中,對某個 Widget 的引用獲取。
偶爾依然會有獲取某個Widget引用的需求;
-
2.引數層層傳遞問題。
如果頂層持有的某個引數需要被傳遞到底層,層層傳遞是一件非常痛苦的事。如果能直接拿到上層 Widget 的引用,獲取該 Widget 持有的引數的話就很方便了。
-
3.當觸發更新的點和要被更新的點在程式碼上距離較遠時。
若沒有藉助 Redux 等框架, 通常我們會將【觸發更新的點】和【被更新的點】封裝在儘可能小的範圍裡(封裝在一個範圍最小的StatefulWidget中)。但總有十萬八千里的兩個有緣人,這個時候觸發大範圍,甚至整顆Widget樹的rebuild的話就不是很優雅了。
那這三個點如何解決呢?
在 Navigator 的原始碼體系裡,有兩個關鍵物件對外提供了全域性引用的能力,分別是:Navigator
和 Overlay
,藉助的均是 BuildContext
的 ancestor*
方法向上查詢。
BuildContext
是 Element
的抽象類,所以BuildContext
的查詢也就是在 Element 樹中遍歷查詢需要的元素。
我們看看BuildContext
都提供了哪些方法:
ancestorInheritedElementForWidgetOfExactType --- 向上查詢最近的InheritedWidget的 InheritedElement
inheritFromWidgetOfExactType --- 向上查詢最近的InheritedWidget
ancestorRenderObjectOfType --- 向上查詢最近的給定型別的RenderObject
ancestorStateOfType --- 向上查詢最近的給定型別的StatefulWidget的State物件
ancestorWidgetOfExactType --- 向上查詢最近的給定型別的Widget
findRenderObject --- 向下查詢當前的RenderObject
rootAncestorStateOfType --- 向上查詢最頂層的給定型別的 State
visitAncestorElements(bool visitor(Element element)) --- 向上遍歷 Ancestor
複製程式碼
向上查詢較為簡單,傳入對應型別即可,向下BuildContext
也提供了遍歷 child的方法:
visitChildElements(ElementVisitor visitor) --- 向下遍歷 child
複製程式碼
以Overlay
的靜態方法of
方法為例(Navigator
也有類似的of
方法),傳入需要查詢的型別物件TypeMatcher
,向上查詢到最近的OverlayState
,使得Overlay
無需層層向下傳遞自己的引用,底層 Widget 遍可以在任何地方拿到Overlay
引用,並呼叫它的方法或屬性,這解決1``2
的問題:
class Overlay {
...
static OverlayState of(BuildContext context, { Widget debugRequiredFor }) {
final OverlayState result = context.ancestorStateOfType(const TypeMatcher<OverlayState>());
...
return result;
}
...
複製程式碼
那麼對於相聚千里之外的有緣人,如何通知對方 rebuild 呢? Overlay
也給了我們很好的示例,以 OverlayState
的insert
方法為例:
通過Overlay
的靜態方法of
獲取到OverlayState
引用之後,呼叫insert
,其內部直接呼叫了setState(() {}
方法修改了自己的資料內容,並觸發了自己範圍內的 rebuild 。
void insert(OverlayEntry entry, { OverlayEntry above }) {
entry._overlay = this;
setState(() {
final int index = above == null ? _entries.length : _entries.indexOf(above) + 1;
_entries.insert(index, entry);
});
}
複製程式碼
這樣,1``2``3
點便有了相應的解決方案,開發過程中不妨考慮用這樣的方式優化你的程式碼。
但在Element樹中遍歷查詢引用以及操作,畢竟不是一件高效和安全的事情,所以在某些場景下,可以考慮下面的一個技巧:“元素自治”。
元素自治
最佳示例依然來自於Overlay
。
傳統程式設計思維方式中,集合負責儲存元素,元素持有資料,某個 Manager 負責操作集合與集合裡的元素。
OverlayState
提供了三個操作集合的方法:
void insert(OverlayEntry entry, { OverlayEntry above })
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry above })
void _remove(OverlayEntry entry)
複製程式碼
受限於 Flutter 宣告式的程式設計方式,物件引用的查詢成本較高,Manager 的實現在這個場景裡也不夠優雅。所以雖然insert
方法依然需要通過Overlay
的靜態方法of
查詢OverlayState
引用來呼叫。 但_remove
卻是一個私有方法,不允許你直接通過OverlayState
來呼叫。
OverlayEntry
的刪除只能由OverlayEntry
自己來執行:
class OverlayEntry {
...
void remove() {
final OverlayState overlay = _overlay;
_overlay = null;
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
overlay._remove(this);
});
} else {
overlay._remove(this);
}
}
...
複製程式碼
這樣的程式設計方式,既保證了元素“安全”,又避免了在樹中的查詢的損耗。
安全的理由是:元素的刪除不僅是從集合中刪除就結束了,還有一系列“解除安裝”和回撥的需要被執行,元素自治遮蔽了外部直接操作集合刪除元素的可能。
私有類包裝:隔離邏輯與Widget
如果需要你來自定義一個Widget,這個Widget內部持有了多個children,這些children都是在不斷變化的。你會怎麼維護這個children列表呢?
直接建立一個 List<Widget>
集合嗎?不,這並不優雅也不安全!
我們知道 Widget 是 Flutter 裡的基礎基石,每一個Widget在run起來之後都有無限的延伸,可能是短命不可複用的可能又是長期存在的,它們不但可以產出Element和RenderObject,還包括了完整的生命周和體系內的各種回撥。
你可以保證能照顧好他們嗎?
Flutter 世界裡的一個潛規則是:Wideget的建立,儘可能只在build方法中進行!將Wideget的建立和銷燬交給Flutter 系統來維護!
那麼該如何做呢?
第三個示例還是來自於Overlay
, Overlay
的設計真的不錯!
Overlay
內部也持有了多個children:List<OverlayEntry>
,但OverlayEntry
並不是一個Widget,它只是一個普通的 Dart 類。它持有了建立Widget必要的屬性以及一些邏輯。
而Overlay
在build時真正建立的 Widget 是_OverlayEntry
:
class _OverlayEntry extends StatefulWidget {
_OverlayEntry(this.entry)
: assert(entry != null),
super(key: entry._key);
final OverlayEntry entry;
@override
_OverlayEntryState createState() => _OverlayEntryState();
}
class _OverlayEntryState extends State<_OverlayEntry> {
@override
Widget build(BuildContext context) {
return widget.entry.builder(context);
}
void _markNeedsBuild() {
setState(() { /* the state that changed is in the builder */ });
}
}
複製程式碼
可以看到_OverlayEntry
是一個私有類,它的程式碼非常簡單,構造方法裡傳入一個OverlayEntry
,build 時執行的是entry.builder(context)
方法。
所以:如果你需要對一個Widget或Widget集合做頻繁的操作,建議的做法是將邏輯和屬性抽離出來,維護一個不變的邏輯物件,讓Widget根據邏輯物件進行build或rebuild。 儘量避免直接操作一個Widget以及改變它內部的屬性。
閱讀 Navigator 原始碼之後對實際應用開發的幫助
原始碼的閱讀往往可以加深對系統執行過程的理解,在將來的某一天可能會起到至關重要的作用,卻也可能永遠用不到。這種收益的不確定性和原始碼閱讀的枯燥性,往往會讓大部分人望而卻步。
所以在文章的最後,我簡單的列出一些在原始碼閱讀之後,對實際應用開發的幫助。由此,來增加你對原始碼學習的積極性。
路由動態監聽
隨著開發複雜度的上升,你一定會有監聽路由變化的需求。如果你對MaterialApp
有些許研究,會知道在構建MaterialApp
時可以傳入一個navigatorObservers
的引數,大概像這樣:
Widget build(BuildContext context) {
return new MaterialApp(
navigatorObservers: [new MyNavigatorObserver()],
home: new Scaffold(
body: new MyPage(),
),
);
}
複製程式碼
navigatorObservers
是一個List<NavigatorObserver>
集合,每當navigator發生變動時,都會遍歷這個集合回撥對應的方法。
即使你不知道MaterialApp
有這樣一個屬性,在閱讀NavigatorState
原始碼時,pop
,push
等方法內部都有下面這樣的程式碼, 瞭解到路由的變化是提供了observer
的:
@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {
...
for (NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute);
...
}
複製程式碼
另一個問題來了!
一個標準的工程,往往會將MaterialApp
申明在最頂層,而大部分需要監聽路由變動的場景,都在下層的業務程式碼裡。笨辦法是將監聽函式層層傳遞,但這絕對是一個極其痛苦的過程。
一個相對優雅的解決方案是:動態新增路由監聽。那如何實現呢?
Navigator
和NavigatorState
並沒有直接暴露新增監聽的介面(是官方並不建議嗎?),但看過原始碼的你會知道,最終回撥的observers
是由Navigator
持有的observers
物件,幸好它是一個public屬性。
所以,動態新增路由監聽的方法可以這樣實現:
MyNavigatorObserver myObserver = MyNavigatorObserver();
@override
void initState() {
super.initState();
//建議在initState時動態新增Observer,而不是build時,避免重複新增
Navigator.of(context).widget.observers.add(myObserver);
}
@override
void dispose() {
super.dispose();
//dispose時記得移除監聽
Navigator.of(context).widget.observers.remove(myObserver);
}
複製程式碼
路由監聽中,識別 彈窗 or Page
一個較為困擾的事情是,在 Flutter 的世界中,無論是頁面還是彈窗,都是以路由的方式來實現的,所以在路由監聽的回撥中,彈窗的展示和消失也會觸發回撥。
如果你想識別回撥中的路由是彈窗還是Page該怎麼辦? 有沒有什麼較為優雅的方式?
讀過原始碼的你一定記得,在OverlayState
的 build 方法中,通過OverlayEntry
的opaque
屬性,將所有將要進入_Theatre
元件中的entry
區分為了onstageChildren
和offstageChildren
。
opaque
意義在哪呢?它決定了當前的Widget是否是一個“全屏不透明”的Widget,Page一般情況下佔用全部螢幕,所以他是“全屏不透明的”,彈窗一般情況下只佔用全部螢幕的一部分,所以它的“全屏透明的”。
讀過原始碼的你會知道,Route
的子類TransitionRoute
持有了opaque
屬性,並且所有的"PageRoute"opaque=true
,"PopupRoute"opaque=false
。
那麼事情就很簡單了:
class MyNavigatorObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic> previousRoute) {
if ((previousRoute is TransitionRoute) && previousRoute.opaque) {
//全屏不透明,通常是一個page
} else {
//全屏透明,通常是一個彈窗
}
}
}
複製程式碼
值得注意的是,opaque
值並不能完全代表它是一個Page或彈窗,總是會有特殊情況的。所以對這一點的理解,更準確的說法是:識別previousRoute
是否會佔據全部螢幕,導致原本的route
不可見。
動態新增 Widget
受限於Flutter 獨特的程式設計方式,想要在程式碼中隨時插入一個 Widget 還是比較困難的。
但讀過原始碼的你已經知道了,在MaterialApp
中已經預先內建了一個Overlay
,雖然它是給 Navigator
服務的,但你也完全可以拿來用:
//獲取最近的一個Overlay進行操作,如果你沒有新增自定義的,通常是`Navigator`的那個
Overlay.of(context).insert(entry);
//獲取最靠近根部的Overlay進行操作,通常是`Navigator`的那個
(context.rootAncestorStateOfType(const TypeMatcher<OverlayState>()) as OverlayState).insert(entry);
複製程式碼
易踩的坑:多Navigator巢狀情況下的錯誤路由查詢
成也 Widget,敗也 Widget。萬物皆 Widget 可以有無限的組合,但也可能導致 Widget 的濫用。
MaterialApp
在 Flutter 世界中的地位類似於 Android 中的 Application
+ BaseActivity
。 理論上一個專案中只應該在頂層有唯一的一個MaterialApp
,但 Flutter 卻也不限制你在 Widget 樹中任意地方使用多個MaterialApp
。
另外Navigator
也是一個 Widget,你也可以在樹中的任意地方插入任意多個Navigator
。
這會造成什麼問題呢?假設我們有這樣一個 Widget 樹:
- MaterialApp
- ...
- Navigator
- ...
- MaterialApp
- Navigator
- ...
- MaterialApp
複製程式碼
你猜這個 Widget 樹裡有多少個 Navigator
? 看過原始碼你知道每個MaterialApp
內部都包含一個Navigator
,所以這棵樹裡有5個Navigator
。 這麼多Navigator
的問題在哪呢?
看下Navigator
的push
方法:
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
return Navigator.of(context).push(route);
}
複製程式碼
預設呼叫的是單個引數的Navigator.of(context)
,在看下of
內部:
static NavigatorState of(
BuildContext context, {
bool rootNavigator = false,
bool nullOk = false,
}) {
final NavigatorState navigator = rootNavigator
? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
: context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
return navigator;
}
複製程式碼
預設情況下,向上查詢的不是根節點的NavigatorState
,而是最近的一個。這將導致你在樹中任意位置push
或pop
操作的可能不是同一個NavigatorState
物件,他們維護的也不是同一個 route
棧,這將導致很多問題。
所以合適的做法是:
-
1.儘可能保證你的程式碼中,
MaterialApp
在專案中有且只有一個,且在Widget 樹的頂層。 -
2.你不能保證程式碼中只有一個
Navigator
, 所以對於全域性的Page管理,建議將push
或pop
封裝,使用Navigator.of(context, rootNavigator:true)
程式碼去保證你拿的是根部的Navigator
。
而對於真的有需要去獲取樹中的某個Navigator
而不是根Navigator
,你要嚴格 check Navigator.of(context)
中你所使用的 BuildContext
,要保證它是在你要獲取的 Navigator
之下的。
一個 badcase:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
MyNavigator(),
GestureDetector(
onTap: (){
Navigator.of(context);
},
child: Text("ClickMe"),
)
],
);
}
}
複製程式碼
MyNavigator
是我們自定義的Navigator
,我們需要點選“ClickMe”來在樹中查詢到MyNavigator
的引用,那麼,你覺得能查的到嗎?
答案是不能!因為我們是基於MyWidget
的BuildContext
(BuildContext
就是Element
的抽象)去在Element樹中向上查詢的,但很明顯MyWidget
在 MyNavigator
上層,當然不會得到你想要的結果啦~