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