Update: Fixed an issue with the timing of data retrieval across components. Check it out in the of(context) method section.

preface

Recently, I saw this problem with some students who were new to Flutter when they made a page jump.

flutter: Navigator operation requested with a context that does not include a Navigator.
flutter: The context used to push or pop routes from the Navigator must be that of a widget that is a
flutter: descendant of a Navigator widget.
Copy the code

The code looks like this

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: FlatButton(
              onPressed: () {
                Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => SecondPage()));
              },
              child: Text('jump')),),),); }}class SecondPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnScaffold( appBar: AppBar(), ); }}Copy the code

At first glance, it doesn’t seem like a problem, and the solution is simple: just pull out the Home part as a new Widget.

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    returnMaterialApp( home: FirstPage(), ); }}class FirstPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
        body: Center(
          child: FlatButton(
              onPressed: () {
                Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => SecondPage()));
              },
              child: Text('jump')))); }}Copy the code

But it must have been very confusing when you first encountered these things. What is BuildContext and why do we pass BuildContext every time we need to build a function? Why does my Navigator operation not find the Navigator in the current context? Why does it make a new widget?

So today I want to share with you along the way how to understand and use BuildContext in Flutter. There are also areas of the widget building process that will be covered, so let’s briefly explain these concepts before we start.

What is Navigator, what does the MaterialApp do

We often open a bunch of pages in an app, and when we return, it goes back to the last open page, and then it goes back layer by layer, and yes it’s a stack. In Flutter, the page stack is managed and maintained by the Navigator.

Push a new page to the screen of navigator.of (context). Push removes the top page of the route from navigator.of (context).popCopy the code

Usually when we build an application we don’t manually create a Navigator that can also navigate the page, and that’s why.

Yes, this Navigator is exactly what MaterialApp provides for us. However, if home, Routes, onGenerateRoute and onUnknownRoute are all null and Builder is not null, the MaterialApp will not create any Navigator.

Now that we know what Navigator and MaterialApp do, let’s look at BuildContext.

BuildContext

Every time we write part of the interface code, we do it in the build function. The build function needs to pass in a BuildContext by default. Let’s see what it is.

abstract class BuildContext {
  /// The current configuration of the [Element] that is this [BuildContext].
  Widget get widget;

  /// The [BuildOwner] for this context. The [BuildOwner] is in charge of
  /// managing the rendering pipeline for this context.
  BuildOwner getowner; .Copy the code

We can see that BuildContext is an abstract class, but what is passed in each build function. Let’s take a look at what happens when you build a view.

How does Flutter build views

In Flutter, Everything is Widget, we write the UI interface by nesting widgets with constructors. In fact, a Widget is not really something to display on the screen, it’s just configuration information, it’s always immutable, and it can be reused in multiple places. So what’s the view tree that’s actually displayed on the screen? Element Tree!

So let’s take a look at what happens when you build your view. Take the Stateless Widget as an example.

abstract class StatelessWidget extends Widget {
  const StatelessWidget({ Key key }) : super(key: key);
  @override
  StatelessElement createElement() => StatelessElement(this); .Copy the code

When the widget is loaded into the view tree, createElement is first invoked and the current widget is passed to Element.

So let’s see what this StatelessElement is

class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true; rebuild(); }}Copy the code

As you can see, StatelessElement preserves the widget’s reference by passing it into its constructor and calls the Build method.

The build method actually calls the Widget’s build method and passes in this, the StatelessElement object. We know that the build method needs to pass in a BuildContext. Why do we pass in StatelessElement? So we keep looking.

class StatelessElement extends ComponentElement.abstract class ComponentElement extends Element.abstract class Element extends DiagnosticableTree implements BuildContext 
Copy the code

It’s actually the Element class that implements BuildContext and is inherited by ComponentElement -> StatelessElement.

So let’s go back to the official explanation for BuildContext:

BuildContextobjects are actually Element objects. The BuildContextinterface is used to discourage direct manipulation of Element objects.

The BuildContext object is actually an Element object, and the BuildContext interface is used to prevent direct operations on Element objects.

Cool! Now we finally know where this BuildContext comes from. Let’s review again what the Flutter construction view does.

View tree loading process

StatelessWidget

  • It first calls the createElement method of the StatelessWidget and generates a StatelesseElement object from the widget.
  • Mount the StatelesseElement object to the Element tree.
  • The StatelesseElement object calls the Widget’s build method and passes the Element itself as BuildContext.

StatefulWidget

  • The createElement method of the StatefulWidget is also called first, and the StatefulElement object is generated from the widget and the widget reference is retained.
  • Mount the StatefulElement to the Element tree.
  • CreateState based on the widget’s createState method.
  • The StatefulElement object calls state’s build method and passes Element itself as BuildContext.

So the context we use in the build function is the Element object created by the current widget.

Method of (context)

This code is often used in Flutter

// Open a new page
Navigator.of(context).push
// Open the Scaffold Drawer
Scaffold.of(context).openDrawer
// Get the display1 style text theme
Theme.of(context).textTheme.display1
Copy the code

So what is this of context? Let’s take the example of a Navigator opening a new page.

static NavigatorState of(
    BuildContext context, {
      bool rootNavigator = false.bool nullOk = false, {})// Key code -----------------------------------------v
    
    final NavigatorState navigator = rootNavigator
        ? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
        : context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
        
// Key code ----------------------------------------^
    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

As you can see, the key code section through the context. RootAncestorStateOfType traverse upward Element tree, and find a matching NavigatorState recently. In other words, “of” is really an encapsulation of the data that the context gets across components.

Our Navigator’s push is done by finding the NavigatorState.

Not only that, BuildContext has many ways to get objects across components

AncestorInheritedElementForWidgetOfExactType (Type targetType) - > InheritedElement ancestorRenderObjectOfType (TypeMatcher The matcher) - > RenderObject ancestorStateOfType (TypeMatcher matcher) - > State ancestorWidgetOfExactType (Type targetType) - > Widget findRenderObject() → RenderObject inheritFromElement(InheritedElement ancestor) {Object aspect}) - > InheritedWidget inheritFromWidgetOfExactType (Type targetType, }) → InheritedWidget rootAncestorStateOfType(TypeMatcher matcher) → State Visitorelements (bool Visitor (Element Element) → void visitChildElements(ElementVisitor visitor) → voidCopy the code

It is important to note that if we need to establish long-term contact with ancestors Inherit object, in initState dependOnInheritedWidgetOfExactType series method can’t call, In order to ensure that the widgets Inherit values change update itself correctly, please State. The method invocation of didChangeDependencies stage.

Review questions

Now let’s see if the current context doesn’t contain a Navigator is easy.

class  MyApp  extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: FlatButton(
              onPressed: () {
                Navigator.of(context).push(
                    MaterialPageRoute(builder: (context) => SecondPage()));
              },
              child: Text('jump')),),),); }}Copy the code

When we use navigator. of(context) in the build function, the context is actually an Element object created from the MyApp widget, However, when the of method searches for the ancestor node upward (the ancestor node of MyApp), there is no MaterialApp, so there is no Navigator provided by it.

So when we split that Scaffold into another widget, we got the FirstPage BuildContext in the Build function of FirstPage and went up to the MaterialApp and found the Navigator it provided. So you can jump to the page happily.

The resources

  • Flutter Widgets101: This is the official video of the Flutter team that explains how statelessWidgets and StatefulWidgets are created.

Write in the last

If the article is wrong, please also point out that you are welcome to leave a message in the comments section below and my email [email protected], I will contact you within 24 hours!

What is the next one has not thought well, may be a real combat series, anyway, we are a lot of zi porcelain. 😌