引言
在使用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。














网友评论