Navigator的正确打开方式

引言

在使用Flutter进行页面间跳转时,Flutter官方给的建议是使用NavigatorNavigator也很友好的提供了pushpushNamedpop等静态方法供咱们选择使用。这些接口的使用方法都不算难,可是咱们会常常碰到下面这个异常。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("搜索页"),
    );
  }
}
复制代码

和第一个例子相比较,咱们将MaterialApphome属性对应的widget(Scaffold)单独拎出来放到AppPage这个widget里面,而后让MaterialApphome属性引用改成AppPage。这个时候,让咱们再次点击搜索🔍按钮,能够看到从首页正常的跳转到了搜索页面。 this

源码分析

异常问题解决了,可是解决的有点糊里糊涂,有点莫名其妙。下面咱们将从源码入手,完全搞清楚该问题的一个来龙去脉。spa

咱们就从点击搜索🔍按钮这个动做开始分析。点击搜索🔍按钮时,调用了Navigatorpush方法。

static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
}
复制代码

push方法调用了Navigatorof方法。

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为空,并且nullOkfalse时,就会抛出一个FlutterError的错误。看一下错误信息,这不正是咱们要寻找的异常问题么?nullOk默认是false的,那也就是说当navigator为空时,就会抛出该异常。

那咱们就找找看,为何navigator会为空。继续往上看,navigator是由context执行不一样的方法返回的。因为咱们并无主动赋值rootNavigator,所以navigator是由context执行ancestorStateOfType方法返回的。

BuildContext-1

上面所说的context是一个BuildContext类型对象,而BuildContext是一个接口类,其最终的实现类是Element。因此在BuildContext声明的ancestorStateOfType接口方法,在Element中能够找到其实现方法。

在讲解ElementancestorStateOfType方法前,咱们要知道WidgetElement的对应关系,能够参考一下这篇文章 Flutter之Widget层级介绍。在这里能够简单的认为每个Widget对应一个Element

再结合上面第一个例子,context就是MyAppbuild方法中的contextMyApp是一个StatelessWidget,而StatelessWidget对应着StatelessElement

在最初讲BuildContext的时候谈到,contextBuildContext类型,而其最终实现类是Element。因此,咱们接着看ElementancestorStateOfType方法。

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类型而且经过matcherState校验的一个Element对象,而后将该对象的State对象返回。

结合Navigatorof方法,这里的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方法中又调用了RenderObjectToWidgetAdapterattachToRenderTree方法。这里的RenderObjectToWidgetAdapter其实是一个Widget,而返回的_renderViewElementElement。也就是说这至关于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;
}
复制代码

elementnull,则经过调用createElement建立element对象。

RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
复制代码

element对象类型为RenderObjectToWidgetElement,而后调用了mount方法,将两个空对象传递进去。也就是说RenderObjectToWidgetElement对象的父Elementnull。记住这一点,后面会用到这个结论。

说到这里,咱们得出一个结论:

App的顶部Widget和其对应的顶部Element分别是RenderObjectToWidgetAdapterRenderObjectToWidgetElement,它的子WidgetMyApp

也就是说,MyApp这个Widget对应的Element,其父ElementRenderObjectToWidgetElement。这个结论回答了BuildContext-1这一小节最后提出的那个问题。

BuildContext-2

让咱们再次回到BuildContextancestorStateOfType方法,也就是ElementancestorStateOfType方法。

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方法这一小节的结论咱们得知,因为当前的ElementMyApp对应的Element,那么_parent就是RenderObjectToWidgetElement,进入while循环,因为RenderObjectToWidgetElement并非StatefulElement类型,则继续找到RenderObjectToWidgetElement的父Element。从main方法这一小节的分析可知,RenderObjectToWidgetElement的父Elementnull,从而推出while循环,继而ancestorStateOfType返回null

也就是说Navigatorof方法中的navigatornull

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;
}
复制代码

在上面Navigatorof方法中,咱们了解到在nullOk默认为false的状况下,为了保证不抛出FlutterError异常,必须保证navigator不为空。也就是说context.ancestorStateOfType必须返回一个NavigatorState类型的navigator

上面已经分析了MyApp这个Widget对应的Element,其父ElementRenderObjectToWidgetElement

那么咱们从MyApp这个Widget出发,分析一下其子Widget树。

从修改后的例子能够看出,MyApp的子WidgetMaterialApp。而MaterialApp的子WidgetMaterialAppbuild方法决定。

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的子WidgetScrollConfiguration。而ScrollConfigurationchild赋值为result对象,这里的resultWidgetsApp,从而获得ScrollConfiguration的子WidgetWidgetsApp

以此类推分析下去,获得下面一条树干(前一个Widget是后一个Widget的父Widget):

MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme

而这里的AnimatedTheme就是上面MaterialAppbuild方法中定义的AnimatedTheme。那么它的子Widget(child属性)就是WidgetsAppbuilder属性传递进来的。而builder属性是在WidgetsApp对应的WidgetsAppStatebuild方法用到。

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, ), ), ); } 复制代码

能够看到,在WidgetsAppStatebuild方法中调用了widget.builder属性,咱们重点关注第二个参数,它是一个Navigator类型的Widget,正是这个参数传递过去并做为了AnimatedTheme的子Widget。结合上面Navigatorof方法逻辑,咱们知道必须找到一个NavigatorState类型的对象。这里的Navigator就是一个StatefulWidget类型,而且对应着一个NavigatorState类型对象。

若是咱们继续往下分析,就能看到这样的一条完整树干:

MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->AnimatedTheme->Navigator->......->AppPage

你们也能够经过调试的方法来验证上述的结论,以下图所示。

因为这条树干太长,所以只截取了部分。能够看到上部分的顶端是AppPage,下部分的底端是MyApp,而中间是Navigator

因为MaterialApp的子Widget一定包含Navigator,而MaterialApphome属性返回的Widget一定是Navigator的子Widget

因此由上述的分析得出以下结论:

若是在Widget中须要使用Navigator导航,则必须将该Widget必须做为MaterialApp的子Widget,而且context(其实是Element)也必须是MaterialApp对应的context的子context

参考文章

Flutter | 深刻理解BuildContext

相关文章
相关标签/搜索