Gonçalo Palma
April 11, 2020

Understanding BuildContext

In Material Design, when we want to give some subtle feedback to the user, we may be inclined to use a SnackBar:

Snackbar

If we are using a Scaffold in our screens, we can easily show it by using the showSnackBar of the context as follows:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: MaterialButton(
          child: Text(
            "SnackBar",
            style: TextStyle(color: Colors.white),
          ),
          color: Colors.red,
          onPressed: () => Scaffold.of(context).showSnackBar(
            SnackBar(
              content: Text("Oh! You clicked me!"),
            ),
          ),
        ),
      ),
    );
  }
}

But this will result in the following error:

flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following assertion was thrown while handling a gesture:
flutter: Scaffold.of() called with a context that does not contain a Scaffold.
flutter: No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). This
flutter: usually happens when the context provided is from the same StatefulWidget as that whose build
flutter: function actually creates the Scaffold widget being sought.
flutter: There are several ways to avoid this problem. The simplest is to use a Builder to get a context that
flutter: is "under" the Scaffold. For an example of this, please see the documentation for Scaffold.of():
flutter:   https://api.flutter.dev/flutter/material/Scaffold/of.html
flutter: A more efficient solution is to split your build function into several widgets. This introduces a
flutter: new context from which you can obtain the Scaffold. In this solution, you would have an outer widget
flutter: that creates the Scaffold populated by instances of your new inner widgets, and then in these inner
flutter: widgets you would use Scaffold.of().
flutter: A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, then use the
flutter: key.currentState property to obtain the ScaffoldState rather than using the Scaffold.of() function.
════════════════════════════════════════════════════════════════════════════════════════════════════

We now have two possible solutions:

Following the first route, we may be inclined to access the showSnackbar method, which resides in the ScaffoldState, with a GlobalKey:

class MyHomePage extends StatelessWidget {
	// The GlobalKey will allow us to access the Scaffold's State
  final GlobalKey<ScaffoldState> _key = GlobalKey<ScaffoldState>();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
		// We add the key to the Scaffold so we can access it later
      key: _key,
      body: Center(
          child: MaterialButton(
            child: Text(
              "SnackBar",
              style: TextStyle(color: Colors.white),
            ),
            color: Colors.red,
			   // We now can access the currentState via the GlobalKey
            onPressed: () => _key.currentState.showSnackBar(
              SnackBar(
                content: Text("Oh! You clicked me!"),
              ),
            ),
          ),
        ),
    );
  }
}

Though this works, the GlobalKey documentation page states that: “Global keys are relatively expensive”, so we should avoid using them when we can find another solution.

The Tale of Two BuildContexts

So what’s in the error message that was displayed above?

/No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). This usually happens when the context provided is from the same StatefulWidget as that whose build function actually creates the Scaffold widget being sought./

What is happening is that both the MaterialButton and the Scaffold are being built by the same BuildContext as we can see in the following diagram:

Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue

Inspecting the Scaffold.of(context) method:

static ScaffoldState of(BuildContext context, { bool nullOk = false }) {
    final ScaffoldState result = context.findAncestorStateOfType<ScaffoldState>();
    if (nullOk || result != null)
      return result;
    throw FlutterError.fromParts(/* ... */);
  }

The findAncestorStateOfType method describes in its name what its purpose is - to find a Widget that is above in the Widget tree whose State’s matches the ScaffoldState. This can be done since the BuildContexthas the notion of parent, and with it we can scale up the tree. To understand how it works, we look at the Element class, which is responsible for instantiating Widgets and implements BuildContext:

T findAncestorStateOfType<T extends State<StatefulWidget>>() {
	  assert(_debugCheckStateIsActiveForAncestorLookup());
    Element ancestor = _parent;
    while (ancestor != null) {
      if (ancestor is StatefulElement && ancestor.state is T)
        break;
      ancestor = ancestor._parent;
    }
    final StatefulElement statefulAncestor = ancestor;
    return statefulAncestor?.state;
  }

Since each Element is aware of its _parent widget, we can navigate up the tree and do a type check to see if we have found the ancestor we were looking for. If we have, then we can return it’s state via statefulAncestor.state.

Looking at our diagram, we can clearly see now that if the MaterialButton is at the same level that the Scaffold then when the findAncestorStateOfType method is called, the ScaffoldState will not be found. So what can we do to solve it?

If we want to separate our UI elements into several classes, one way we can do it is to create a separate StatelessWidget which will build the MaterialButton separately:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CustomButton(),
      ),
    );
  }
}

/// We separate the UI by creating a new StatelessWidget
class CustomButton extends StatelessWidget {
	
	/// This build method will create a new `BuildContext` that will have as an ancestor the `Scaffold`
  @override
  Widget build(BuildContext context) {
    return MaterialButton(
      child: Text(
        "SnackBar",
        style: TextStyle(color: Colors.white),
      ),
      color: Colors.red,
		// We can now safely find the ancestor `Scaffold` via the `BuildContext`
      onPressed: () => Scaffold.of(context).showSnackBar(
        SnackBar(
          content: Text("Oh! You clicked me!"),
        ),
      ),
    );
  }
}

Which can be translated in the following diagram:

Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue

When the Buttoncalls tries to find an ancestor with the CustomButton’s BuildContext, it will find the Scaffold as a direct ancestor, so the findAncestorStateOfType find a ScaffoldStatewhich we can access to call the showSnackBar method.

But what if we want to keep it all in the same StatelessWidget? That’s where the Builder Widget comes in. It provides us with a WidgetBuilder callback that provide us a new BuildContext for we to use:

class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
		// The `Builder` widget will provide a `builder` method with its own `BuildContext` so that we can access any ancestor above it with this new context
      body: Builder(
              builder: (secondContext) => Center(
          child: MaterialButton(
            child: Text(
              "SnackBar",
              style: TextStyle(color: Colors.white),
            ),
            color: Colors.red,
			  // Using the `Builder` `BuildContext`, named `secondContext` we can reach the `Scaffold` ancestor.
            onPressed: () => Scaffold.of(secondContext).showSnackBar(
              SnackBar(
                content: Text("Oh! You clicked me!"),
              ),
            ),
          ),
        ),
      ),
    );
  }
}

This will create exactly the same Widget-BuildContext graph that we have seen above, with the main difference that now we have it all inside the same class.

Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue

Why should we take care of our BuildContext

We now know that the BuildContext can be used to locate our widget and other widgets in the tree. But the question is - why should we care?

Let’s assume that we created an initial screen in which a Scaffold accepts a StatelessWidget as a body. As with the previous section, we will want to show a SnackBar with some text.

We also decide to abstract the onPressed method, but since we don’t have access to the BuildContext in a StatelessWidget, we cache it:

class CachedContextPage extends StatelessWidget {
	/// The cached instance of the BuildContext
  BuildContext _context;

	/// Shows a snackbar with the cached BuildContext
  void showSnackbar() {
    Scaffold.of(_context).showSnackBar(
      SnackBar(
        content: Text(
          "Yay!",
        ),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    print("Called build");
    // Caching the context when this Widget is first built
    if (_context == null) {
      _context = context;
    }

    return Center(
      child: RaisedButton(
            color: Colors.red,
            child: Text(
              "Show Snackbar",
              style: TextStyle(color: Colors.white),
            ),
            onPressed: () => showSnackbar()),
    );
  }
}

Though this works as intended, we may ask: what happens if the Widget tree changes and it no longer has a Scaffold Ancestor?

Let’s look at the following example: we have two buttons in the screen:

Example app showing SnackBar and changing the root Scaffold from Material to Cupertino

class MyHomePage extends StatefulWidget {
  @override
  _MyDeprecatedContextPageState createState() =>
      _MyDeprecatedContextPageState();
}

class _My MyHomePageState extends State<MyHomePage> {
	/// The child Widget
  Widget _child;

  bool _isMaterial = true;

  @override
  void initState() {
    super.initState();
    _child = PageBody();
  }

  @override
  Widget build(BuildContext context) {
    /// We check if the `isMaterial` flag is true to either return a `Scaffold` or a `CupertinoPageScaffold` as the root Widget
    if (_isMaterial) {
      return _getMaterialScaffold(_child);
    }
    return _getCupertinoScaffold(_child);
  }

  Widget _getMaterialScaffold(Widget child) => Scaffold(
        appBar: AppBar(
          title: Text("This is a text"),
        ),
        body: child,
      );

  Widget _getCupertinoScaffold(Widget child) => CupertinoPageScaffold(
        child: child,
        navigationBar: CupertinoNavigationBar(
          backgroundColor: Colors.red,
          middle: Column(
            children: <Widget>[
              Icon(Icons.warning),
              Text(
                "Changed Theme!",
                style: TextStyle(color: Colors.white),
              )
            ],
          ),
        ),
      );

	/// Public method to change the current root widget
  void changeType() {
    setState(() => _isMaterial = !_isMaterial);
  }
}

This Widget exposes the changeTypemethod that will force a rebuild with the different style.

The PageBody will contain the two buttons and the methods to access both the Scaffold, to show a SnackBar, and the MyDeprecatedContextPage so that we can access the public method changeType. As with before, since it’s a StatelessWidget, we opt to cache the BuildContext

class LeakedPageBody extends StatelessWidget {
  BuildContext _context;

  void showSnackbar() {
    Scaffold.of(_context).showSnackBar(
      SnackBar(
        content: Text(
          "Yay!",
        ),
      ),
    );
  }

  void changePageType() {
    _context
        .findAncestorStateOfType<_MyHomePageState>()
        .changeType();
  }

  @override
  Widget build(BuildContext context) {
    print("Called build");
    // Caching the context
    if (_context == null) {
      _context = context;
    }

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: <Widget>[
        RaisedButton(
            color: Colors.red,
            child: Text(
              "Show Snackbar",
              style: TextStyle(color: Colors.white),
            ),
            onPressed: () => showSnackbar()),
        SizedBox(
          height: 20,
        ),
        RaisedButton(
            color: Colors.blue,
            child: Text(
              "Change Style",
              style: TextStyle(color: Colors.white),
            ),
            onPressed: () => changePageType()),
      ],
    );
  }
}

We can click on the “Show Snackbar” button once, and it shows the SnackBar correctly. However, when we click the Change Style button, the root widget of MyHomePagewill be changed to a CupertinoPageScaffoldthat cannot be accessed via the Scaffold.of(_context) method.

As predicted, when clicking again in the Show Snackbar button again, the error that we have encountered before is thrown:

flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following assertion was thrown while handling a gesture:
flutter: Scaffold.of() called with a context that does not contain a Scaffold.
flutter: No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). This
flutter: usually happens when the context provided is from the same StatefulWidget as that whose build
flutter: function actually creates the Scaffold widget being sought.
flutter: There are several ways to avoid this problem. The simplest is to use a Builder to get a context that
flutter: is "under" the Scaffold. For an example of this, please see the documentation for Scaffold.of():
flutter:   https://api.flutter.dev/flutter/material/Scaffold/of.html
flutter: A more efficient solution is to split your build function into several widgets. This introduces a
flutter: new context from which you can obtain the Scaffold. In this solution, you would have an outer widget
flutter: that creates the Scaffold populated by instances of your new inner widgets, and then in these inner
flutter: widgets you would use Scaffold.of().
flutter: A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, then use the
flutter: key.currentState property to obtain the ScaffoldState rather than using the Scaffold.of() function.

This does not surprise us, since we have seen that the Scaffold.of() method uses the findAncestorStateOfType that now will not find a ScaffoldState in the widget tree.

What’s more interesting is that if we click again in the Change Type button, it will not work and in turn it throws the following error:

flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following assertion was thrown while handling a gesture:
flutter: Looking up a deactivated widget's ancestor is unsafe.
flutter: At this point the state of the widget's element tree is no longer stable.
flutter: To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by
flutter: calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.

As we can see from the error message, when we use the cached BuildContext it is now referencing a deactivated widget.

Looking at the findAncestorStateOfType method again, we verify that the in the first line there is a call to:

assert(_debugCheckStateIsActiveForAncestorLookup());

This will in turn verify if the Element is in active state or not:

  bool _debugCheckStateIsActiveForAncestorLookup() {
    assert(() {
      if (_debugLifecycleState != _ElementLifecycle.active) {
        throw FlutterError.fromParts(/* */);
      }
      return true;
    }());
    return true;
  }

This means that, while the parent was rebuilding, the cached BuildContext was marked as not active, and we will no longer be able to look up ancestors in the tree. To learn more about how Flutter builds its UI elements, specifically Widgets, Elements and Render Objects, we can check the talk by Mark Sullivan and Andrew Fitz - “How Flutter renders Widgets” to understand how this works.

The final question is: how can we solve it? And, as we may guess, the solution is to always use the BuildContext provided by the build method:

class PageBody extends StatelessWidget {
  void showSnackbar(BuildContext context) {
    Scaffold.of(context).showSnackBar(
      /* */
    );
  }

  void changePage(BuildContext context) {
    context
        .findAncestorStateOfType<_MyDeprecatedContextPageState>()
        .changeType();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      /* */
      children: <Widget>[
        RaisedButton(
            /* */
			   // We now pass the `BuildContext` from the `build` method directly to the method
            onPressed: () => showSnackbar(context)),
        SizedBox(
          height: 20,
        ),
        RaisedButton(
            /* */
			   // We now pass the `BuildContext` from the `build` method directly to the method
            onPressed: () => changePage(context)),
      ],
    );
  }
}

And this way, when the root widget changes from a Scaffold to a CuppertinoPageScaffold, the build method of the PageBody is called again to provide it with the new BuildContext that can then be used in each RaisedButton callback.

Conclusion

In this article we’ve seen why we should care about our BuildContext:

There is still much to be learned about BuildContext, specifically how we can depend on an InheritedWidget and why updating it forces our Widget to rebuild. But that will be a topic for a future article.

Follow me!

I often share some small insights on Flutter 💙