引言
在使用Flutter
进行页面间跳转时,Flutter
官方给的建议是使用Navigator
。Navigator
也很友好的提供了push
、pushNamed
、pop
等静态方法供我们选择使用。这些接口的使用方法都不算难,但是我们会经常碰到下面这个异常。
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.
翻译过来的意思是路由跳转功能所需的context没有包含Navigator。路由跳转功能所需的context对应的widget必须是Navigator这个widget的子类。
究竟是啥意思呢?让人看得是一头雾水啊。没有什么高深的知识是一个例子解决不了的,下面我们将通过一个例子来探究这个异常的前因后果。
一个例子
下面这个例子将通过点击搜索🔍按钮,实现跳转到搜索页的功能。
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("搜索页"),
);
}
}
上面这个例子是有问题的,当我们点击首页的搜索🔍按钮时,在控制台上会打印出上面所提到的异常信息。
我们将上面的例子稍微做一下转换。
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
。这个时候,让我们再次点击搜索🔍按钮,可以看到从首页正常的跳转到了搜索页面。
源码分析
异常问题解决了,但是解决的有点糊里糊涂,有点莫名其妙。下面我们将从源码入手,彻底搞清楚该问题的一个前因后果。
我们就从点击搜索🔍按钮这个动作开始分析。点击搜索🔍按钮时,调用了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
方法返回的。
BuildContext-1
上面所说的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方法
我们知道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-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方法这一小节的结论我们得知,由于当前的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
异常。
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
默认为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
。