When you develop Flutter applications, you barely get around seeing BuildContext
s 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. theMaterialPageRoute
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:
This is the widget tree describing the visual content in a hierarchical manner as a diagram:
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 BuildContext
s come into play: BuildContext
s 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 BuildContext
s 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 Element
s. 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 BuildContext
s and not about RenderObject
s but for the sake of completion I will say a few words about RenderObject
s as they are only one abstraction level below Element
s.
In fact, RenderObject
s 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 RenderObject
s 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:
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:
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 Element
s that decides whether a repainting of the RenderObject
is necessary based upon the changes to the old version of the Element
.
Franklin Oladipo
Marc
In reply to Franklin Oladipo's comment
Darkdust
Marc
In reply to Darkdust's comment
Azim Otajonov