What is a BuildContext?

What is a BuildContext?

When you develop Flutter applications, you barely get around seeing BuildContexts somewhere in the code. You might intuitively know how to use it, but can you explain its purpose and its meaning in detail? This article aims to clarify this Flutter basic knowledge that seems to confuse not only beginners.

Why is the BuildContext so relevant?

BuildContext appears in a lot of places in the code of Flutter projects:

  • Inside of every build() method of a Widget as the only argument
  • Inside of Navigator.of(), Theme.of(), Scaffold.of() and several other static functions as the only argument
  • Inside of many builder functions (e. g. the MaterialPageRoute Widget)

It seems to always appear whenever something is happening with widgets. This is not very surprising since a widget is a very crucial concept within the Flutter ecosystem, almost everything has to do with. However, it gives us a first clue about its purpose but also about its significance.

It also let’s us know about the relevance: when it is connected to such an essential part of Flutter, a deeper understanding about its principles is needed.

What is the connection between BuildContext and Widgets?

Okay, so far we have only figured out about the BuildContext appearing in many places inside the codes. But what does it do?

Before we clarify this, let’s remind ourselves what widgets are and how they are interconnected. For this purpose we consider a sample app with this screen:

Flutter BuildContext screen sample
The corresponding widget tree

This is the widget tree describing the visual content in a hierarchical manner as a diagram:

Flutter BuildContext widget tree
The corresponding widget tree

A widget is an immutable configuration of the visual representation of rendered elements.

If it’s only a configuration you might ask yourself: how does a widget know about its position inside this widget tree?

This is where BuildContexts come into play: BuildContexts are handles to the location of a widget in the tree. As a consequence, every widget has its own BuildContext as the location inside the widget tree is unique. Furthermore, if a widget has children, parent widget’s context becomes the parent context for the contexts of the child widgets.

It’s worth noting that a widget is only visible to its corresponding BuildContext or the BuildContext of its parents. That makes it easy to locate a parent widget from a child widget.

What exactly do Scaffold.of(…), Theme.of(…), Navigator.of(…) etc. do?

There are numerous classes that provide a static method called of whose only argument is a BuildContext. Examples are MediaQuery, Scaffold, Provider, Theme, Navigator.

All these methods work in a similar fashion. Even their official documentation looks pretty much the same:

“The xxx from the closest instance of this class that encloses the given context”

Where xxx is a placeholder for whatever it is you are calling of on. In Theme it’s data, in Scaffold, it’s ScaffoldState, in Navigator, it’s state. But what exactly does this mean?

In the case of the above widget tree, if we called Scaffold.of() inside the AppBar widget, the method would climb up the widget tree one step and return a ScaffoldState representing the Scaffold in this very location of the tree. If we found ourselves inside the ListTile widget, it would take two steps up. The point is: it looks for the nearest widget (where distance is defined in the context of a tree as the number of edges the algorithm has to visit) of the given type and returns it.

What would happen if there was no context?

If there was no BuildContext, it would be pretty cumbersome to pass data down the widget tree. You would be unable to use methods like Scaffold.of(...) meaning that you would have to pass the reference to this widget down the widget tree manually to every constructor. Widget trees build up pretty quickly and a depth of more than 10 is very common. Just imagine the necessary boilerplate code for the sole purpose of making a Scaffold widget available in the grand-grand-….-grand-child-widget.

Technically, most of the implementations of the of methods internally call dependOnInheritedWidgetOfExactType, which is a method being defined on the BuildContext class.

Concepts like InheritedWidget also rely on this specific part of the framework. Most of the state management solutions build upon that. So there would be hardly an state management solution apart from managing state via manual injection and callback functions.

Why is the BuildContext available everywhere inside a StatefulWidget but not inside a StatelessWidget?

You might have noticed that the BuildContext is directly usable in every method of State class when using StatefulWidget. However, if you are using a StatelessWidget, this is not the case. You have to pass it down the tree you are building.

That’s because a StatelessWidget is immutable. Every member variable of the class must be final and thus can not be altered after the object has been constructed. The BuildContext, however, is known later, when the build() method is called.

Use BuildContext in initState()

When you try to use the BuildContext in the initState() function of a StatefulWidget, you might stumble across some issues. The documents say:

You cannot use BuildContext.dependOnInheritedWidgetOfExactType from this method.

This means if you are trying to access the part of the widget tree above the current widget inside initState() e. g. if you use the Provider package or other state management solutions, this will not work.

They also propose a possible solution:

However, didChangeDependencies will be called immediately following this method, and BuildContext.dependOnInheritedWidgetOfExactType can be used there.

So depending on what you want to do, you might want to move your code into didChangeDependencies(). Be aware, though, that other than initState(), didChangeDependencies() might be called more than once during the lifecycle of the widget.

Another option if you really want to stick to initState() is to use the SchedulerBinding Mixin:

1SchedulerBinding.instance.addPostFrameCallback((_) {
2  // The code you wanted to call in initState()
3});

Can we create a BuildContext?

Okay, now that we clarified the relevance of BuildContexts the question that could pop up: (how) can we create it?

Technically, a BuildContext is just a usual Dart class. However, this class is marked abstract which means that there is no way to directly create an instance of (instantiate a) BuildContext by a constructor call. Dart does not have the syntactic concept of interfaces like other languages (e. g. Java, C#, …). Instead, abstract classes are in use, which makes BuildContext work as an interface.

This interface is implemented by Elements. This way, the developers are prevented from directly accessing Elements. Instead, they work with the interface. If every Element implements BuildContext, we can say: every Element is a BuildContext which might sound counter-intuitive when reading it the first time.

Whenever a widget is inserted into the tree, the build method of the widget is called (by the framework) with the respective BuildContext (in most cases an Element) as the only argument.

What is an element?

Now I have introduced this new term without actually explaining it: Element. Let me make up for this.

If you inspect the code of a widget, you will not find anything related to drawing pixels on the screen. So like I said above: a widget is nothing else but a configuration for the visual representation of the elements on the screen. Like a blueprint for what’s actually displayed.

That’s also why a high frame rate is not a big deal for Flutter because a widget is (re)created very quickly since it’s a lightweight container. The heavy lifting is done somewhere else.

To a certain degree it’s similar to the virtual DOM in web frameworks like React where actual DOM manipulations only happen when there was really a change that affects the visuals.

In Flutter it works this way: during the initial creation of a widget, an Element is created alongside (we say: the widget is inflated to an Element) and inserted into the element tree. Meaning there is a widget tree and an element tree. The widget tree contains all configurations and the element tree the rendered widgets. Whenever the widget changes, the framework compares the old widget do the new one and updates the element respectively. While the widget is rebuilt, the element is only updated (as a complete rebuild would require a lot of computational power).

But to make things clear: actual painting does also not happen here. This is what happens in a RenderObject.

What is a RenderObject?

This article is about BuildContexts and not about RenderObjects but for the sake of completion I will say a few words about RenderObjects as they are only one abstraction level below Elements.

In fact, RenderObjects are what the visual pieces on the screen correspond to. Their purpose is to define areas on the screen regarding their spatial dimensions. They are referenced by the element. As a consequence we are dealing with another (third) tree here: the RenderObjects together form a tree which is called Render Tree whose root node is a RenderView (being a variant of a RenderObject).

The above explanations can be visually represented like this diagram:

A diagram showing the widget tree, the element tree and the render tree
The trees and their connection

The widgets are what we developers see in the code: properly named parts of our UI. They are referenced by the elements which they create once they are inflated. The elements also reference the RenderObject which hold information about the visuals and contain drawing logic. Element sort of bridge the gap between widgets (the structural part that configures the UI) and RenderObjects (the visual part).

What to consider when working with async

Depending on how strict your linter is configured, you might stumble across the warning: use_build_context_synchronously. This happens if you do something like this:

1await Navigator.of(context).pushNamed(
2    MyPage.routeName,
3  );
4
5  Navigator.of(context).pop();

The problem here is that there is an async operation going on (pushNamed). async always means that you don’t know in advance how long it is going to take or even if it ever finishes.

The line below that where the very same context is used again, is problematic because if there is an undefined amount of time between the first call where the context is used and the second one, how can you be sure the context does even still exist? After all, there are many changes per second in the Element Tree. If the context is for example a BottomSheet, the sheet could be long dismissed which effectively removes it from the tree.

The solution is not to store the context but the class the context is bound to. In this case: the NavigatorState being return from Navigator.of(context) (as described above):

1final navigator = Navigator.of(context);
2await navigator.pushNamed(
3    MyPage.routeName,
4);
5navigator.pop();

By keeping the reference to the NavigatorState we ensure that the context is kept in memory across the call to the async method.

Conclusion

BuildContext can barely be explained in one sentence. Although it mostly just appears as an argument of the build method, the underlying mechanics are a lot more complex.

BuildContext is an interface which is implement by Element, the base class for a variety of different Element types which are being created when a widget inflates.

Element itself connects the widget (which is just an immutable configuration) to the RenderObject that is responsible for the actual painting on the screen.

This leads to three trees: Widget Tree, Element Tree and Render Tree.

Semantically speaking, the BuildContext is the handle to the location of a widget in the tree. It can be used to find widgets of a certain type in the tree and is the requirement for most of the state management solutions.

The reason for this separation is driven by performance needs: recreating a widget is done in no time, as it’s only a configuration object. Repainting on the other hand can significantly influence the FPS. That’s why there is the intermediate layer in form of Elements that decides whether a repainting of the RenderObject is necessary based upon the changes to the old version of the Element.

Comments (4) ✍️

Franklin Oladipo

A very nice article. Very explanatory.
Reply to Franklin Oladipo

Marc
In reply to Franklin Oladipo's comment

Thank you. Glad I could expand your knowledge.
Reply to Marc

Darkdust

Hallo, einer der wenigen Artikel. Die BuildContext in der Tiefe erklären. Besten Dank !
Reply to Darkdust

Marc
In reply to Darkdust's comment

Sehr gerne! Freut mich, dass der Artikel dir geholfen hat :).
Reply to Marc

Comment this 🤌

You are replying to 's commentRemove reference