引言
在使用Flutter進(jìn)行頁面間跳轉(zhuǎn)時(shí),Flutter官方給的建議是使用Navigator。Navigator也很友好的提供了push、pushNamed、pop等靜態(tài)方法供我們選擇使用。這些接口的使用方法都不算難,但是我們會(huì)經(jīng)常碰到下面這個(gè)異常。
Navigator operation requested with a context that does not include a Navigator.
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.
翻譯過來的意思是路由跳轉(zhuǎn)功能所需的context沒有包含Navigator。路由跳轉(zhuǎn)功能所需的context對應(yīng)的widget必須是Navigator這個(gè)widget的子類。
究竟是啥意思呢?讓人看得是一頭霧水啊。沒有什么高深的知識是一個(gè)例子解決不了的,下面我們將通過一個(gè)例子來探究這個(gè)異常的前因后果。
一個(gè)例子
下面這個(gè)例子將通過點(diǎn)擊搜索??按鈕,實(shí)現(xiàn)跳轉(zhuǎn)到搜索頁的功能。
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("搜索頁"),
);
}
}
上面這個(gè)例子是有問題的,當(dāng)我們點(diǎn)擊首頁的搜索??按鈕時(shí),在控制臺(tái)上會(huì)打印出上面所提到的異常信息。
我們將上面的例子稍微做一下轉(zhuǎn)換。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
/// 首頁
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: AppPage(),
);
}
}
/// 將第一個(gè)例子中的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("搜索頁"),
);
}
}
和第一個(gè)例子相比較,我們將MaterialApp的home屬性對應(yīng)的widget(Scaffold)單獨(dú)拎出來放到AppPage這個(gè)widget里面,然后讓MaterialApp的home屬性引用改為AppPage。這個(gè)時(shí)候,讓我們再次點(diǎn)擊搜索??按鈕,可以看到從首頁正常的跳轉(zhuǎn)到了搜索頁面。
源碼分析
異常問題解決了,但是解決的有點(diǎn)糊里糊涂,有點(diǎn)莫名其妙。下面我們將從源碼入手,徹底搞清楚該問題的一個(gè)前因后果。
我們就從點(diǎn)擊搜索??按鈕這個(gè)動(dòng)作開始分析。點(diǎn)擊搜索??按鈕時(shí),調(diào)用了Navigator的push方法。
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
return Navigator.of(context).push(route);
}
push方法調(diào)用了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時(shí),就會(huì)拋出一個(gè)FlutterError的錯(cuò)誤。看一下錯(cuò)誤信息,這不正是我們要尋找的異常問題么?nullOk默認(rèn)是false的,那也就是說當(dāng)navigator為空時(shí),就會(huì)拋出該異常。
那我們就找找看,為什么navigator會(huì)為空。繼續(xù)往上看,navigator是由context執(zhí)行不同的方法返回的。由于我們并沒有主動(dòng)賦值rootNavigator,因此navigator是由context執(zhí)行ancestorStateOfType方法返回的。
BuildContext-1
上面所說的context是一個(gè)BuildContext類型對象,而BuildContext是一個(gè)接口類,其最終的實(shí)現(xiàn)類是Element。所以在BuildContext聲明的ancestorStateOfType接口方法,在Element中可以找到其實(shí)現(xiàn)方法。
在講解Element的ancestorStateOfType方法前,我們要知道Widget和Element的對應(yīng)關(guān)系,可以參考一下這篇文章 Flutter之Widget層級介紹。在這里可以簡單的認(rèn)為每一個(gè)Widget對應(yīng)一個(gè)Element。
再結(jié)合上面第一個(gè)例子,context就是MyApp的build方法中的context。MyApp是一個(gè)StatelessWidget,而StatelessWidget對應(yīng)著StatelessElement。
在最初講BuildContext的時(shí)候談到,context是BuildContext類型,而其最終實(shí)現(xiàn)類是Element。所以,我們接著看Element的ancestorStateOfType方法。
State ancestorStateOfType(TypeMatcher matcher) {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element ancestor = _parent;
while (ancestor != null) {
if (ancestor is StatefulElement && matcher.check(ancestor.state)) /// 直到找到一個(gè)StatefuleElement對象并通過matcher的State校驗(yàn)
break;
ancestor = ancestor._parent;
}
final StatefulElement statefulAncestor = ancestor;
return statefulAncestor?.state;
}
ancestorStateOfType做的事情并不復(fù)雜,主要是沿著其父類一直往上回溯,直到找到一個(gè)StatefulElement類型并且通過matcher的State校驗(yàn)的一個(gè)Element對象,然后將該對象的State對象返回。
結(jié)合Navigator的of方法,這里的matcher對象為TypeMatcher<NavigatorState>()。
問題:那么當(dāng)前StatelessElement的_parent是什么呢?這就要從入口方法main開始說起了。
main方法
我們知道m(xù)ain()方法是程序的入口方法。
void main() => runApp(MyApp());
main方法通過調(diào)用runApp方法接收一個(gè)widget。
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
runApp方法中調(diào)用了attachRootWidget方法。這里的參數(shù)app就是MyApp這個(gè)widget。
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget, ///這里的rootWidget是MyApp
).attachToRenderTree(buildOwner, renderViewElement);
}
attachRootWidget方法中又調(diào)用了RenderObjectToWidgetAdapter的attachToRenderTree方法。這里的RenderObjectToWidgetAdapter實(shí)際上是一個(gè)Widget,而返回的_renderViewElement是Element。也就是說這相當(dāng)于App的頂部Widget和其對應(yīng)的頂部Element。
注意第一次調(diào)用時(shí),
attachToRenderTree方法的renderViewElement參數(shù)為null,而且rootWidget(MyApp)是作為RenderObjectToWidgetAdapter的子Widget傳遞進(jìn)去。
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,則通過調(diào)用createElement創(chuàng)建element對象。
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
該element對象類型為RenderObjectToWidgetElement,然后調(diào)用了mount方法,將兩個(gè)空對象傳遞進(jìn)去。也就是說RenderObjectToWidgetElement對象的父Element為null。記住這一點(diǎn),后面會(huì)用到這個(gè)結(jié)論。
說到這里,我們得出一個(gè)結(jié)論:
App的頂部
Widget和其對應(yīng)的頂部Element分別是RenderObjectToWidgetAdapter和RenderObjectToWidgetElement,它的子Widget為MyApp。
也就是說,MyApp這個(gè)Widget對應(yīng)的Element,其父Element是RenderObjectToWidgetElement。這個(gè)結(jié)論回答了BuildContext-1這一小節(jié)最后提出的那個(gè)問題。
BuildContext-2
讓我們再次回到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方法這一小節(jié)的結(jié)論我們得知,由于當(dāng)前的Element是MyApp對應(yīng)的Element,那么_parent就是RenderObjectToWidgetElement,進(jìn)入while循環(huán),由于RenderObjectToWidgetElement并不是StatefulElement類型,則繼續(xù)找到RenderObjectToWidgetElement的父Element。從main方法這一小節(jié)的分析可知,RenderObjectToWidgetElement的父Element為null,從而推出while循環(huán),繼而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這個(gè)條件,所以就拋出了FlutterError異常。
分析到了這里,我們算是回答了第一個(gè)例子為什么會(huì)拋出FlutterError異常的原因,接下來我們分析一下為什么修改后的例子不會(huì)拋出FluterError異常。
Navigator的正確打開方式
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默認(rèn)為false的情況下,為了保證不拋出FlutterError異常,必須保證navigator不為空。也就是說context.ancestorStateOfType必須返回一個(gè)NavigatorState類型的navigator。
上面已經(jīng)分析了MyApp這個(gè)Widget對應(yīng)的Element,其父Element是RenderObjectToWidgetElement。
那么我們從MyApp這個(gè)Widget出發(fā),分析一下其子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。
以此類推分析下去,得到下面一條樹干(前一個(gè)Widget是后一個(gè)Widget的父Widget):
MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme
而這里的AnimatedTheme就是上面MaterialApp的build方法中定義的AnimatedTheme。那么它的子Widget(child屬性)就是WidgetsApp的builder屬性傳遞進(jìn)來的。而builder屬性是在WidgetsApp對應(yīng)的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方法中調(diào)用了widget.builder屬性,我們重點(diǎn)關(guān)注第二個(gè)參數(shù),它是一個(gè)Navigator類型的Widget,正是這個(gè)參數(shù)傳遞過去并作為了AnimatedTheme的子Widget。結(jié)合上面Navigator的of方法邏輯,我們知道必須找到一個(gè)NavigatorState類型的對象。這里的Navigator就是一個(gè)StatefulWidget類型,并且對應(yīng)著一個(gè)NavigatorState類型對象。
如果我們繼續(xù)往下分析,就能看到這樣的一條完整樹干:
MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme->Navigator->......->AppPage。
大家也可以通過調(diào)試的方法來驗(yàn)證上述的結(jié)論,如下圖所示。
由于這條樹干太長,因此只截取了部分??梢钥吹缴喜糠值捻敹耸?code>AppPage,下部分的底端是MyApp,而中間是Navigator。
由于MaterialApp的子Widget必定包含Navigator,而MaterialApp的home屬性返回的Widget必定是Navigator的子Widget。
所以由上述的分析得出如下結(jié)論:
如果在Widget中需要使用Navigator導(dǎo)航,則必須將該Widget必須作為MaterialApp的子Widget,并且context(實(shí)際上是Element)也必須是MaterialApp對應(yīng)的context的子context。