The Unbearable Lightness of BuildContext

Overview

In this post I'd like to write about Flutter's BuildContext, as a mechanism to enable communication throughout the whole widget tree. The idea came to me while trying to explain a fix to a friend, and soon I realized how there was so much more behind it than the fix. I believe that contexts are very important when it comes to mobile development, as they are one of the building blocks of it. So this post shows a real life error and how to solve it, but its main focus is on the idea underlying the fix, the BuildContext.

The problem

Say you've added the navigation drawer to your app, following the recommended tutorial.





@override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: Drawer(
        child: ListView(
          padding: EdgeInsets.zero,
          children: [
            DrawerHeader(
              child: Text('Drawer Header'),
              decoration: BoxDecoration(
                color: Colors.blue,
              ),
            ),
            ListTile(
              title: Text('Drawer Item'),
            ),
          ],
        ),
      ),
      appBar: /* your app bar */,
      body: /* your body */,
      floatingActionButton: /* your fab */,
    );
  }

At this point, say you want to change the app bar icon to something different. Doing so, you'll have to open the navigation drawer programmatically when the icon is clicked










appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.android),
          onPressed: () => Scaffold.of(context).openDrawer(),
        ),
        title: Text(widget.title),
      ),

Eventually, upon clicking the app bar icon, the drawer will not open and you'll see this exception:

Scaffold.of() called with a context that does not contain a Scaffold.
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.

Let's try now to understand what the problem means, and what the error message is telling us. The solution is easy, but it's worth to look into this problem to better understand the BuildContext.

Understanding BuildContext

Clearly we've some problem with the BuildContext passed to Scaffold.of(). To better understand what the issue is exactly, let's first see what we know about BuildContext.
The official documentation states two very important facts:
  • A handle to the location of a widget in the widget tree
  • Each widget has its own BuildContext, which becomes the parent of the widget returned by the StatelessWidget.build or State.build function
The former tells us the BuildContext is actually a location in the widget tree; as you most certainly know, Flutter builds widgets based on a widget tree, whereas each widget has one parent, and each parent may have one or many children. You can have a look at the widget tree of your app using the Flutter Inspector:


The latter tells us that each widget has a build context. When a parent widget build some children widgets, their build contexts are bound together in a similar parent-children relationship. 

Representing BuildContext

As the name says, BuildContext is the context used to build widgets. Both StatelessWidget class and StatefulWidget's State class override the build method, which takes one only parameter, the BuildContext, to build its children.

@override
  Widget build(BuildContext context) {
    return /* children */;
  }

One important functionality of BuildContext, is that a child can reach out to a parent, any parent, via the passed context, because the passed context will have a reference to its parent's context, which will have a reference to its parent's context, which will have a reference to its parent's context, and so on until a certain parent is found via its context. Basically is a way for the child to communicate to any of its ancestors.
This functionality to allow communication is needed for instance when a child want to communicate to open or close a page; for instance communicating to close the actual shown page can be done by any children widget, no matter how deep in the tree, with this very simple call:

Navigator.pop(context);

the context passed to pop() helps the Navigator to remove one page from the stack, because the page to be removed is chosen based on the passed context. That is, the page choice is contextualized, meaning that two different children widgets calling that very same simple call will pass two different contexts, which could result into two different pages to be closed. As an example, a simple button's context will make the entire page to be closed, while a dialog button's context will make the dialog to be closed.

Similar to Navigator, other classes use BuildContext for communication between parents and children, like ThemeScaffold, and InheritedWidget.

A second look

Giving a second look at the exception we ended up with, while trying to open the navigation drawer, it's possible that now it makes more sense to us

Scaffold.of() called with a context that does not contain a Scaffold.
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.

The output literally tells us that when clicking the button, we want to communicate the action of opening the drawer to a Scaffold, communication which goes through the given context, and all the way through its ancestors, but no one of them is able to find a Scaffold at all. Let's try to portray why

@override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: /* your drawer */,
      appBar: AppBar(
        leading: IconButton(
          icon: Icon(Icons.android),
          onPressed: () => Scaffold.of(context).openDrawer(),
        ),
        title: Text(widget.title),
      ),
      body: /* your body */,
      floatingActionButton: /* your fab */,
    );
  }

The context passed to the Scaffold.of() is the very same context which is passed to the build() method, which builds the Scaffold itself. The official documentation states:

The state from the closest instance of this class that encloses the given context.

which means that the closest Scaffold reference will be taken, if any, starting from a context which is the parent of the given context. So, the given context, which is also the one which builds the Scaffold, is excluded, and on top of the given context, if you look at the widget tree, there are the contexts of MaterialApp and MyApp, and none of them have a reference to any Scaffold.

The solution

As explained also in Scaffold.of() doc, the solution is relatively simple, and it consists in adding a man-in-the-middle context which will be passed to Scaffold.of(), so that when the method will start to look up starting from its parent, it will actually find the one building the Scaffold. This can be accomplished with the Builder widget, which only purpose is to build its children

@override
  Widget build(BuildContext context) {
    return Scaffold(
      drawer: /* your drawer */,
      appBar: AppBar(
        leading: Builder(
          builder: (context) {
            return IconButton(
              icon: Icon(Icons.android),
              onPressed: () => Scaffold.of(context).openDrawer(),
            );
          },
        ),
        title: Text(widget.title),
      ),
      body: /* your body */,
      floatingActionButton: /* your fab */,
    );
  }

After introducing the Builder to the code, we can see that the context passed to Scaffold.of() is child of the context which builds the Scaffold. So Scaffold.of() will start to look up from the given context's parent, and up through the hierarchy, until it will find one context which can return a meaningful reference of a Scaffold. This way, our IconButton will be able to communicate to the Navigator to open the drawer, without the need to have direct access to it.
An alternative, and more efficient solution is to extract our IconButton into its own widget.

Conclusion

The BuildContext is a very powerful class, and the communication mechanism it provides allows messaging up and down through the hierarchical widget tree without the need to have direct access. From a given point of view, it accomplishes the same mechanism as Provider  or a singleton can do, but been built-in in the framework and only for the specific widgets that support it, like Navigator, Theme, Scaffold and so on.

Comments

Post a Comment