在使用Flutter
進行頁面間跳轉時,Flutter
官方給的建議是使用Navigator
。Navigator
也很友好的提供了push
、pushNamed
、pop
等靜態方法供咱們選擇使用。這些接口的使用方法都不算難,可是咱們會常常碰到下面這個異常。bash
Navigator operation requested with a context that does not include a Navigator.markdown
The context used to push or pop routes from the Navigator must be that of a widget that is a descendant of a Navigator widget.app
翻譯過來的意思是路由跳轉功能所需的context沒有包含Navigator。路由跳轉功能所需的context對應的widget必須是Navigator這個widget的子類。less
到底是啥意思呢?讓人看得是一頭霧水啊。沒有什麼高深的知識是一個例子解決不了的,下面咱們將經過一個例子來探究這個異常的來龍去脈。ide
下面這個例子將經過點擊搜索🔍按鈕,實現跳轉到搜索頁的功能。源碼分析
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); /// 首頁 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: Scaffold( /// Scaffold start body: Center( child: IconButton( icon: Icon( Icons.search, ), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SearchPage(); })); }, ) ), ), /// Scaffold end ); } } /// 搜索頁 class SearchPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("搜索"), ), body: Text("搜索頁"), ); } } 複製代碼
上面這個例子是有問題的,當咱們點擊首頁的搜索🔍按鈕時,在控制檯上會打印出上面所提到的異常信息。post
咱們將上面的例子稍微作一下轉換。ui
import 'package:flutter/material.dart'; void main() => runApp(MyApp()); /// 首頁 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: AppPage(), ); } } /// 將第一個例子中的Scaffold包裹在AppPage裏面 class AppPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( body: Center( child: IconButton( icon: Icon( Icons.search, ), onPressed: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return SearchPage(); })); }, ) ), ); } } /// 搜索頁 class SearchPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("搜索"), ), body: Text("搜索頁"), ); } } 複製代碼
和第一個例子相比較,咱們將MaterialApp
的home
屬性對應的widget
(Scaffold)單獨拎出來放到AppPage
這個widget
裏面,而後讓MaterialApp
的home
屬性引用改成AppPage
。這個時候,讓咱們再次點擊搜索🔍按鈕,能夠看到從首頁正常的跳轉到了搜索頁面。 this
異常問題解決了,可是解決的有點糊里糊塗,有點莫名其妙。下面咱們將從源碼入手,完全搞清楚該問題的一個來龍去脈。spa
咱們就從點擊搜索🔍按鈕這個動做開始分析。點擊搜索🔍按鈕時,調用了Navigator
的push
方法。
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) { return Navigator.of(context).push(route); } 複製代碼
push
方法調用了Navigator
的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>()); assert(() { if (navigator == null && !nullOk) { throw FlutterError( 'Navigator operation requested with a context that does not include a Navigator.\n' 'The context used to push or pop routes from the Navigator must be that of a ' 'widget that is a descendant of a Navigator widget.' ); } return true; }()); return navigator; } 複製代碼
of
方法判斷navigator
爲空,並且nullOk
爲false
時,就會拋出一個FlutterError
的錯誤。看一下錯誤信息,這不正是咱們要尋找的異常問題麼?nullOk
默認是false
的,那也就是說當navigator
爲空時,就會拋出該異常。
那咱們就找找看,爲何navigator
會爲空。繼續往上看,navigator
是由context
執行不一樣的方法返回的。因爲咱們並無主動賦值rootNavigator
,所以navigator
是由context
執行ancestorStateOfType
方法返回的。
上面所說的context
是一個BuildContext
類型對象,而BuildContext
是一個接口類,其最終的實現類是Element
。因此在BuildContext
聲明的ancestorStateOfType
接口方法,在Element
中能夠找到其實現方法。
在講解Element
的ancestorStateOfType
方法前,咱們要知道Widget
和Element
的對應關係,能夠參考一下這篇文章 Flutter之Widget層級介紹。在這裏能夠簡單的認爲每個Widget
對應一個Element
。
再結合上面第一個例子,context
就是MyApp
的build
方法中的context
。MyApp
是一個StatelessWidget
,而StatelessWidget
對應着StatelessElement
。
在最初講BuildContext
的時候談到,context
是BuildContext
類型,而其最終實現類是Element
。因此,咱們接着看Element
的ancestorStateOfType
方法。
State ancestorStateOfType(TypeMatcher matcher) { assert(_debugCheckStateIsActiveForAncestorLookup()); Element ancestor = _parent; while (ancestor != null) { if (ancestor is StatefulElement && matcher.check(ancestor.state)) /// 直到找到一個StatefuleElement對象並經過matcher的State校驗 break; ancestor = ancestor._parent; } final StatefulElement statefulAncestor = ancestor; return statefulAncestor?.state; } 複製代碼
ancestorStateOfType
作的事情並不複雜,主要是沿着其父類一直往上回溯,直到找到一個StatefulElement
類型而且經過matcher
的State
校驗的一個Element
對象,而後將該對象的State
對象返回。
結合Navigator
的of
方法,這裏的matcher
對象爲TypeMatcher<NavigatorState>()
。
問題:那麼當前StatelessElement
的_parent
是什麼呢?這就要從入口方法main
開始提及了。
咱們知道main()方法是程序的入口方法。
void main() => runApp(MyApp());
複製代碼
main
方法經過調用runApp
方法接收一個widget
。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
複製代碼
runApp
方法中調用了attachRootWidget
方法。這裏的參數app
就是MyApp
這個widget
。
void attachRootWidget(Widget rootWidget) { _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>( container: renderView, debugShortDescription: '[root]', child: rootWidget, ///這裏的rootWidget是MyApp ).attachToRenderTree(buildOwner, renderViewElement); } 複製代碼
attachRootWidget
方法中又調用了RenderObjectToWidgetAdapter
的attachToRenderTree
方法。這裏的RenderObjectToWidgetAdapter
其實是一個Widget
,而返回的_renderViewElement
是Element
。也就是說這至關於App的頂部Widget
和其對應的頂部Element
。
注意第一次調用時,
attachToRenderTree
方法的renderViewElement
參數爲null
,並且rootWidget(MyApp)
是做爲RenderObjectToWidgetAdapter
的子Widget傳遞進去。
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) { if (element == null) { owner.lockState(() { element = createElement(); assert(element != null); element.assignOwner(owner); }); owner.buildScope(element, () { element.mount(null, null); }); } else { element._newWidget = this; element.markNeedsBuild(); } return element; } 複製代碼
element
爲null
,則經過調用createElement
建立element
對象。
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
複製代碼
該element
對象類型爲RenderObjectToWidgetElement
,而後調用了mount
方法,將兩個空對象傳遞進去。也就是說RenderObjectToWidgetElement
對象的父Element
爲null
。記住這一點,後面會用到這個結論。
說到這裏,咱們得出一個結論:
App的頂部
Widget
和其對應的頂部Element
分別是RenderObjectToWidgetAdapter
和RenderObjectToWidgetElement
,它的子Widget
爲MyApp
。
也就是說,MyApp
這個Widget
對應的Element
,其父Element
是RenderObjectToWidgetElement
。這個結論回答了BuildContext-1這一小節最後提出的那個問題。
讓咱們再次回到BuildContext
的ancestorStateOfType
方法,也就是Element
的ancestorStateOfType
方法。
State ancestorStateOfType(TypeMatcher matcher) { assert(_debugCheckStateIsActiveForAncestorLookup()); Element ancestor = _parent; while (ancestor != null) { if (ancestor is StatefulElement && matcher.check(ancestor.state)) break; ancestor = ancestor._parent; } final StatefulElement statefulAncestor = ancestor; return statefulAncestor?.state; } 複製代碼
從main方法這一小節的結論咱們得知,因爲當前的Element
是MyApp
對應的Element
,那麼_parent
就是RenderObjectToWidgetElement
,進入while
循環,因爲RenderObjectToWidgetElement
並非StatefulElement
類型,則繼續找到RenderObjectToWidgetElement
的父Element
。從main方法這一小節的分析可知,RenderObjectToWidgetElement
的父Element
爲null
,從而推出while
循環,繼而ancestorStateOfType
返回null
。
也就是說Navigator
的of
方法中的navigator
爲null
。
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>()); assert(() { if (navigator == null && !nullOk) { throw FlutterError( 'Navigator operation requested with a context that does not include a Navigator.\n' 'The context used to push or pop routes from the Navigator must be that of a ' 'widget that is a descendant of a Navigator widget.' ); } return true; }()); return navigator; } 複製代碼
這樣便知足了navigator == null && !nullOk這個條件,因此就拋出了FlutterError
異常。
分析到了這裏,咱們算是回答了第一個例子爲何會拋出FlutterError
異常的緣由,接下來咱們分析一下爲何修改後的例子不會拋出FluterError
異常。
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>()); assert(() { if (navigator == null && !nullOk) { throw FlutterError( 'Navigator operation requested with a context that does not include a Navigator.\n' 'The context used to push or pop routes from the Navigator must be that of a ' 'widget that is a descendant of a Navigator widget.' ); } return true; }()); return navigator; } 複製代碼
在上面Navigator
的of
方法中,咱們瞭解到在nullOk
默認爲false
的狀況下,爲了保證不拋出FlutterError
異常,必須保證navigator
不爲空。也就是說context.ancestorStateOfType
必須返回一個NavigatorState
類型的navigator
。
上面已經分析了MyApp
這個Widget
對應的Element
,其父Element
是RenderObjectToWidgetElement
。
那麼咱們從MyApp
這個Widget
出發,分析一下其子Widget
樹。
從修改後的例子能夠看出,MyApp
的子Widget
爲MaterialApp
。而MaterialApp
的子Widget
由MaterialApp
的build
方法決定。
Widget build(BuildContext context) { Widget result = WidgetsApp( key: GlobalObjectKey(this), navigatorKey: widget.navigatorKey, navigatorObservers: _navigatorObservers, pageRouteBuilder: <T>(RouteSettings settings, WidgetBuilder builder) => MaterialPageRoute<T>(settings: settings, builder: builder), home: widget.home, routes: widget.routes, initialRoute: widget.initialRoute, onGenerateRoute: widget.onGenerateRoute, onUnknownRoute: widget.onUnknownRoute, builder: (BuildContext context, Widget child) { // Use a light theme, dark theme, or fallback theme. ThemeData theme; final ui.Brightness platformBrightness = MediaQuery.platformBrightnessOf(context); if (platformBrightness == ui.Brightness.dark && widget.darkTheme != null) { theme = widget.darkTheme; } else if (widget.theme != null) { theme = widget.theme; } else { theme = ThemeData.fallback(); } return AnimatedTheme( data: theme, isMaterialAppTheme: true, child: widget.builder != null ? Builder( builder: (BuildContext context) { // Why are we surrounding a builder with a builder? // // The widget.builder may contain code that invokes // Theme.of(), which should return the theme we selected // above in AnimatedTheme. However, if we invoke // widget.builder() directly as the child of AnimatedTheme // then there is no Context separating them, and the // widget.builder() will not find the theme. Therefore, we // surround widget.builder with yet another builder so that // a context separates them and Theme.of() correctly // resolves to the theme we passed to AnimatedTheme. return widget.builder(context, child); }, ) : child, ); }, title: widget.title, onGenerateTitle: widget.onGenerateTitle, textStyle: _errorTextStyle, // The color property is always pulled from the light theme, even if dark // mode is activated. This was done to simplify the technical details // of switching themes and it was deemed acceptable because this color // property is only used on old Android OSes to color the app bar in // Android's switcher UI. // // blue is the primary color of the default theme color: widget.color ?? widget.theme?.primaryColor ?? Colors.blue, locale: widget.locale, localizationsDelegates: _localizationsDelegates, localeResolutionCallback: widget.localeResolutionCallback, localeListResolutionCallback: widget.localeListResolutionCallback, supportedLocales: widget.supportedLocales, showPerformanceOverlay: widget.showPerformanceOverlay, checkerboardRasterCacheImages: widget.checkerboardRasterCacheImages, checkerboardOffscreenLayers: widget.checkerboardOffscreenLayers, showSemanticsDebugger: widget.showSemanticsDebugger, debugShowCheckedModeBanner: widget.debugShowCheckedModeBanner, inspectorSelectButtonBuilder: (BuildContext context, VoidCallback onPressed) { return FloatingActionButton( child: const Icon(Icons.search), onPressed: onPressed, mini: true, ); }, ); assert(() { if (widget.debugShowMaterialGrid) { result = GridPaper( color: const Color(0xE0F9BBE0), interval: 8.0, divisions: 2, subdivisions: 1, child: result, ); } return true; }()); return ScrollConfiguration( behavior: _MaterialScrollBehavior(), child: result, ); } 複製代碼
直接看到最後的return
,返回了ScrollConfiguration
。也就是說MaterialApp
的子Widget
是ScrollConfiguration
。而ScrollConfiguration
的child
賦值爲result
對象,這裏的result
是WidgetsApp
,從而獲得ScrollConfiguration
的子Widget
爲WidgetsApp
。
以此類推分析下去,獲得下面一條樹幹(前一個Widget
是後一個Widget
的父Widget
):
MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme
而這裏的AnimatedTheme
就是上面MaterialApp
的build
方法中定義的AnimatedTheme
。那麼它的子Widget
(child屬性)就是WidgetsApp
的builder
屬性傳遞進來的。而builder
屬性是在WidgetsApp
對應的WidgetsAppState
的build
方法用到。
Widget build(BuildContext context) { Widget navigator; if (_navigator != null) { navigator = Navigator( key: _navigator, // If window.defaultRouteName isn't '/', we should assume it was set // intentionally via `setInitialRoute`, and should override whatever // is in [widget.initialRoute]. initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName, onGenerateRoute: _onGenerateRoute, onUnknownRoute: _onUnknownRoute, observers: widget.navigatorObservers, ); } Widget result; if (widget.builder != null) { result = Builder( builder: (BuildContext context) { return widget.builder(context, navigator); }, ); } else { assert(navigator != null); result = navigator; } ...省略 return DefaultFocusTraversal( policy: ReadingOrderTraversalPolicy(), child: MediaQuery( data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), child: Localizations( locale: appLocale, delegates: _localizationsDelegates.toList(), child: title, ), ), ); } 複製代碼
能夠看到,在WidgetsAppState
的build
方法中調用了widget.builder
屬性,咱們重點關注第二個參數,它是一個Navigator
類型的Widget
,正是這個參數傳遞過去並做爲了AnimatedTheme
的子Widget
。結合上面Navigator
的of
方法邏輯,咱們知道必須找到一個NavigatorState
類型的對象。這裏的Navigator
就是一個StatefulWidget
類型,而且對應着一個NavigatorState
類型對象。
若是咱們繼續往下分析,就能看到這樣的一條完整樹幹:
MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme->Navigator->......->AppPage。
你們也能夠經過調試的方法來驗證上述的結論,以下圖所示。
因爲這條樹幹太長,所以只截取了部分。能夠看到上部分的頂端是AppPage
,下部分的底端是MyApp
,而中間是Navigator
。
因爲MaterialApp
的子Widget
一定包含Navigator
,而MaterialApp
的home
屬性返回的Widget
一定是Navigator
的子Widget
。
因此由上述的分析得出以下結論:
若是在Widget
中須要使用Navigator
導航,則必須將該Widget
必須做爲MaterialApp
的子Widget
,而且context
(其實是Element
)也必須是MaterialApp
對應的context
的子context
。