The introduction

When using Flutter to jump between pages, the recommendation given by Flutter officials is to use Navigator. Navigator also provides friendly static methods like Push, pushNamed, and POP for us to choose from. None of these interfaces are difficult to use, but we often encounter the following exception.

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.

The context required for the redirect function does not contain the Navigator. The widget corresponding to the context required for the redirect function must be a subclass of the Navigator widget.

What does that mean? It’s kind of confusing. There is no profound knowledge that cannot be solved by an example, and we will explore the cause and effect of this anomaly through an example.

A case in point

The following example will jump to the search page by clicking the search 🔍 button.

import 'package:flutter/material.dart'; void main() => runApp(MyApp()); Class MyApp extends StatelessWidget {@override Widget build(BuildContext) {return MaterialApp(
      home: Scaffold(    /// Scaffold start
        body: Center(
          child: IconButton(
            icon: Icon(
          	 Icons.search,
        	),
            onPressed: () {
              Navigator.push(context, MaterialPageRoute(builder: (context) {
                returnSearchPage(); })); }, ) ), ), /// Scaffold end ); Class SearchPage extends StatelessWidget {@override Widget build(BuildContext context) {return Scaffold(
      appBar: AppBar(
        title: Text("Search"),
      ),
      body: Text("Search page")); }}Copy the code

The above example is problematic. When we click the search 🔍 button on the home page, the above exception message is printed on the console.

Let’s translate the above example a little bit.

import 'package:flutter/material.dart'; void main() => runApp(MyApp()); Class MyApp extends StatelessWidget {@override Widget build(BuildContext) {returnMaterialApp( home: 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) {
                returnSearchPage(); })); },))); Class SearchPage extends StatelessWidget {@override Widget build(BuildContext context) {return Scaffold(
      appBar: AppBar(
        title: Text("Search"),
      ),
      body: Text("Search page")); }}Copy the code

Compared with the first example, we isolated the Scaffold corresponding to the MaterialApp home property into the AppPage widget and changed the MaterialApp home property reference to AppPage. At this point, let’s click the search 🔍 button again, you can see the normal jump from the home page to the search page.

Source code analysis

The exception problem is solved, but the solution is a bit confusing and confusing. Let’s start with the source code and get to the bottom of one of the causes and consequences of this problem.

Let’s start by clicking the search 🔍 button. When you click the search 🔍 button, the Navigator’s push method is invoked.

static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
}
Copy the code

The push method calls the Navigator’s of method.

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;
}
Copy the code

The of method throws a FlutterError when it determines that the navigator is empty and nullOk is false. Take a look at the error message. Isn’t that the exception problem we’re looking for? NullOk is false by default, which means it will be thrown when the navigator is empty.

So let’s try to figure out why navigator is empty. Looking further up, the navigator is returned by the context performing a different method. Since we didn’t actively assign rootNavigator, the navigator is returned by context executing the ancestorStateOfType method.

BuildContext-1

Context is a BuildContext object, and BuildContext is an interface class whose final implementation class is Element. So the ancestorStateOfType interface method of the BuildContext declaration can be found in Element.

Before looking at the Element ancestorStateOfType method, we need to know the relationship between widgets and elements. See the introduction to the Widget hierarchy of Flutter in this article. It is easy to think of each Widget as having an Element.

In combination with the first example above, context is the context in the build method of MyApp. MyApp is a StatelessWidget, and StatelessWidget corresponds to StatelessElement.

When we first talked about BuildContext, context is a BuildContext type, and its final implementation class is Element. So, let’s move on to Element’s ancestorStateOfType method.

State ancestorStateOfType(TypeMatcher matcher) {
    assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;
    while(ancestor ! = null) {if(ancestor is StatefulElement && matcher. State) /// until a StatefuleElement object is found and matcher's state is verifiedbreak;
      ancestor = ancestor._parent;
    }
    final StatefulElement statefulAncestor = ancestor;
    returnstatefulAncestor? .state; }Copy the code

AncestorStateOfType doesn’t have to be complicated, basically tracing its parent class up until it finds an Element object of StatefulElement type that has passed Matcher’s State check, and then returns that object’s State object.

In combination with the Navigator’s of method, the matcher object here is TypeMatcher
().

Question: So the currentStatelessElementthe_parentWhat is it? This is done from the entry methodmainHere we go.

The main method

We know that the main() method is the entry method to the program.

void main() => runApp(MyApp());
Copy the code

The main method receives a widget by calling the runApp method.

void runApp(Widget app) { WidgetsFlutterBinding.ensureInitialized() .. attachRootWidget(app) .. scheduleWarmUpFrame(); }Copy the code

The attachRootWidget method is called in the runApp method. The parameter app here is the MyApp widget.

void attachRootWidget(Widget rootWidget) {
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]', child: rootWidget, // attachToRenderTree(buildOwner, renderViewElement); }Copy the code

AttachRootWidget method calls again RenderObjectToWidgetAdapter attachToRenderTree method. RenderObjectToWidgetAdapter here is actually a Widget, and the returned _renderViewElement is Element. This is equivalent to the top Widget of your App and its corresponding top Element.

Pay attention to the first call, attachToRenderTree method renderViewElement parameter is null, and rootWidget (MyApp) is as RenderObjectToWidgetAdapter child widgets.

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;
}
Copy the code

If element is null, the Element object is created by calling createElement.

RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
Copy the code

The element object types as RenderObjectToWidgetElement, then call the mount method, two empty object passed into it. That is to say RenderObjectToWidgetElement object’s parent Element is null. Keep that in mind, and we’ll use that later.

Having said this, we can draw a conclusion:

App at the top of the Widget and its corresponding to the top of the Element are respectively RenderObjectToWidgetAdapter RenderObjectToWidgetElement and its child widgets for MyApp.

That is to say, the MyApp this Widget corresponding Element, Element is RenderObjectToWidgetElement his father. This conclusion answers the question posed at the end of the BuildContext-1 section.

BuildContext-2

Let’s go back to BuildContext’s ancestorStateOfType method, which is Element’s ancestorStateOfType method.

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;
    returnstatefulAncestor? .state; }Copy the code

We’ve learned from the main method the conclusion of this section, due to the current Element is MyApp corresponding Element, then _parent is RenderObjectToWidgetElement, into a while loop, Because not StatefulElement RenderObjectToWidgetElement type, continued to find RenderObjectToWidgetElement parent Element. From the main method of this section analysis shows that RenderObjectToWidgetElement parent Element is null, which launched the while loop, which in turn ancestorStateOfType returns null.

That is, the Navigator is null in the Navigator’s of method.

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;
}
Copy the code

This satisfies navigator == null &&! NullOk, so FlutterError is thrown.

So with this analysis, we kind of answered the first example why was it thrownFlutterErrorWhy does the modified example not throwFluterErrorThe exception.

The correct opening of 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;
}
Copy the code

In the Navigator’s of method above, we learned that in order not to throw FlutterError when nullOk is false by default, the Navigator must not be empty. That is to say, the context. AncestorStateOfType must return a NavigatorState types of the navigator.

It has analyzed the MyApp this Widget corresponding Element, Element is RenderObjectToWidgetElement his father.

So let’s start with the MyApp Widget and examine its Widget tree.

As you can see from the modified example, the child Widget of MyApp is MaterialApp. The Child widgets of the MaterialApp are determined by the Build method of the MaterialApp.

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: As a result, all the information we had got got got better than the information we had collected. } return true; } ()); return ScrollConfiguration( behavior: _MaterialScrollBehavior(), child: result, ); }Copy the code

You see the return at the end, which returns the ScrollConfiguration. That is, the child Widget of the MaterialApp is ScrollConfiguration. The child of ScrollConfiguration is assigned to a Result object, where result is WidgetsApp, resulting in WidgetsApp as the child Widget of ScrollConfiguration.

And so on, we get the following tree trunk (the first Widget is the parent Widget of the second Widget) :

MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->A nimatedTheme

The AnimatedTheme here is the AnimatedTheme defined in the Build method of the MaterialApp above. Its child Widget (child property) is passed in from the Builder property of WidgetsApp. The Builder property is used in the build method of WidgetsAppState corresponding to WidgetsApp.

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; }... Omit the return DefaultFocusTraversal (policy: ReadingOrderTraversalPolicy (), the child: MediaQuery (data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), child: Localizations( locale: appLocale, delegates: _localizationsDelegates.toList(), child: title, ), ), ); }Copy the code

As you can see, the WidgetsAppState build method calls the widget. Builder property. We’ll focus on the second parameter, which is a Navigator widget. It is this parameter that is passed in as a child Widget of the AnimatedTheme. Combining the Navigator’s of method logic above, we know that we must find an object of type NavigatorState. The Navigator here is of type StatefulWidget and corresponds to an object of type NavigatorState.

If we go further, we can see a complete tree trunk like this:

MyApp->MaterialApp->ScrollConfiguration->WidgetsApp->DefaultfocusTraversal->MediaQuery->Localizations->Builder->Title->A nimatedTheme->Navigator->…… – > AppPage.

You can also verify the above conclusion through debugging method, as shown in the figure below.

As the trunk was too long, only part of it was taken. You can see that the top part is AppPage, the bottom part is MyApp, and the middle part is Navigator.

Since the Child Widget of the MaterialApp must contain the Navigator, the Widget returned by the MaterialApp home property must be the child Widget of the Navigator.

Therefore, the following conclusions can be drawn from the above analysis:

If theWidgetIs required to useNavigatorNavigation, must be theWidgetMust be asMaterialAppThe son ofWidgetAnd,contextAs a matter of factElement) must beMaterialAppThe correspondingcontextThe son ofcontext.

Refer to the article

Understanding the Flutter | BuildContext