I’m 100% sure that every single Flutter dev knows BuildContext
. We can’t build apps without this object given as an argument of build()
methods. Navigator.of()
, showDialog()
, or a lot of other APIs require BuildContext
as an argument.
But how do you describe what BuildContext
is? Do you know everything about it?
In this article, I’m going to describe what I know about BuildContext
as much as possible.
This article will cover a lot of topics about BuildContext
regardless of how important it is. So sit back on your sofa and relax when reading this.
Then, let's get started with Element
.
Element
Element
is the most important component in the Flutter framework.
Element
does a lot of work at the very center of the Flutter framework, and we have to know about it before talking about BuildContext
.
Flutter has a concept of “Everything is a Widget”, as we know, however, this slogan only describes how an interface for us app developers should look like. Widget is an immutable object that preserves values to construct UI, but that's it. It doesn't have any logic to manage rebuilding or calculating layout.
Then who does that?
Here Element
comes in, and it manages all the processes for building UI under the hood.
Every widget creates its corresponding Element
during the process of building. While widgets are instantly disposed and created, Elements
are reused as much as possible in the Flutter framework as long as they are at the same position on the widget tree.
Thanks to reused Elements
, rebuilding is optimized and done efficiently even if we don’t take care of how much we cause rebuilding with setState()
or other methods.
Element also preserves references to their parents and children. Once we take a look at its source code in framework.dart
, we soon find a field of Element? _parent
as well as a method named visitChildren()
, both of which are for visiting their ancestors and descendants.
Although we usually call the tree a “widget tree”, Element constructs a tree in reality.
That's a brief description of Element
.
Element implements BuildContext
Now, let’s take a look at the declaration of Element class below.
abstract class Element extends DiagnosticableTree implements BuildContext {
}
We can see, Element implements BuildContext
, where we can say BuildContext
is Element
that we discussed so far.
As we discussed above, Element has a lot of business. It preserves the references to its parents and children, optimizes rebuilding, and also preserves references to RenderObject
, State
, or other objects for building UI.
Thus, we Flutter devs are not supposed to touch Element
directly to keep the concept of “Everything is a widget”. But still, we sometimes need ways to find ancestor(or descendant) widgets. This is why BuildContext
is provided.
The interface class of BuildContext
limits the interfaces, or methods and fields in other words, of Element
to hide inappropriate methods to be shown to us. All the methods BuildContext
makes visible are related to operations on the widget tree, such as dependOnInheritedWidgetOfExactType
, findAncestorStateOfType
, etc.
In spite of allowing us to call all the operations defined in Element
, which causes confusion and unexpected behavior, BuildContext
limits the methods and provides us with relevant entry points to access the power of the widget tree without considering the existence of Element
under the hood.
How BuildContext is delivered to us
We usually get BuildContext
as an argument of build()
method of StatefulWidget
or StatefulWidget
's subclass.
class MyWidget extends StatelessWidget {
const MyWidget({super.key});
// context is given as an argument of build() method
@override
Widget build(BuildContext context) {
return Container();
}
}
Now, let's take a look at how this build()
method is called and where BuildContext
is given.
You can easily find the answer by running your app and configuring a breakpoint at any build()
method you implemented.
Once you stop your application at the breakpoint, you can see the "CALL STACK" section in the debugger.
The second line of the call stack would be StatelessElement.build
()
. You can jump to the source code of StatelessElement
by clicking the line, and it says:
class StatelessElement extends ComponentElement {
StatelessElement(StatelessWidget super.widget);
@override
Widget build() => (widget as StatelessWidget).build(this);
}
The argument of build()
method is this
, which is StatelessElement
in this case. Because StatelessElement
is a subclass of Element
, we can say that BuildContext
is an Element
corresponding to the widget.
This is how BuildContext
is delivered to us.
How BuildContext makes Navigator.of() functional
Now, let's move on to the topic of how Navigator.of()
works.
The implementation of Navigator.of()
is as follows:
static NavigatorState of(
BuildContext context, {
bool rootNavigator = false,
}) {
NavigatorState? navigator;
if (context case StatefulElement(:final NavigatorState state)) {
navigator = state;
}
navigator = rootNavigator
? context.findRootAncestorStateOfType<NavigatorState>() ?? navigator
: navigator ?? context.findAncestorStateOfType<NavigatorState>();
return navigator!;
}
Using given context
, this calls findRootAncestorStateOfType<NavigatorState>
or findAncestorStateOfType<NavigatorState>
inside. What's that?
findAncestorStateOfType<T extends State>()
is a method defined in BuildContext
interface and implemented in Element
class.
@override
T? findAncestorStateOfType<T extends State<StatefulWidget>>() {
Element? ancestor = _parent;
while (ancestor != null) {
if (ancestor is StatefulElement && ancestor.state is T) {
break;
}
ancestor = ancestor._parent;
}
final StatefulElement? statefulAncestor = ancestor as StatefulElement?;
return statefulAncestor?.state as T?;
}
This method tries to find the ancestor Element
with the given type T
and returns the State
of the found Element
.
To find the ancestor Element
, it starts with referring to its parent Element
by _parent
. If the _parent
is not the one we are looking for, it continues to refer to the parent of the _parent
until it finds the ancestor Element
of type T
using while
loop.
Then, once the ancestor Element
is found, it returns the State
of the found Element
. That is how findAncestorStateOfType()
works.
What Navigator.of()
is doing is only calling findAncestorStateOfType()
or similar findRootAncestorStateOfType()
giving NavigatorState
as the type parameter. Those methods are exposed by BuildContext
interface, and that's why we have to provide BuildContext
as an argument of Navigator.of()
.
DefaultTextStyle.of()
We have another .of()
method here, DefaultTextStyle.of()
.
This looks similar to Navigator.of()
, but its internal implementation is different.
Because understanding the difference between Navigator.of()
and DefaultTextStyle.of()
is important to know the Flutter framework's fundamental concept of StatefulWidget
and InheritedWidget
, I'll explain in more detail about those methods.
The source code of DefaultTextStyle.of()
is so simple as follows:
static DefaultTextStyle of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<DefaultTextStyle>() ?? const DefaultTextStyle.fallback();
}
It only calls dependOnInheritedWidgetOfExactType<DefaultTextStyle>()
method. But what it does inside? Let's take a look at the source code of dependOnInheritedWidgetOfExactType()
as well.
As dependOnInheritedWidgetOfExactType()
is also defined in BuildContext
interface and implemented in Element
class, we can just open the source code of Element
class to check its implementation.
@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
final InheritedElement? ancestor = _inheritedElements?[T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T;
}
_hadUnsatisfiedDependencies = true;
return null;
}
Here, it tries to find the ancestor Element
with the given type T
from _inheritedElements
map.
This map is defined as the type of PersistentHashMap<Type, InheritedElement>
and preserves all the InheritedElement
s, which are associated Element
s of InheritedWidget
s, in the widget tree.
Because this finds the target Element
from the map, it is faster than findAncestorStateOfType()
method which uses while
loop to find the target Element
. In other words, the calculation cost of dependOnInheritedWidgetOfExactType()
is independent of the depth of the widget tree and is O(1) while the cost of findAncestorStateOfType()
is O(n), which means it is affected by the depth of the widget tree.
Pros and Cons
Navigator.of()
and DefaultTextStyle.of()
are both trying to find the target Element
by traversing the widget tree using the given BuildContext
. But DefaultTextStyle.of()
, InheritedWidget
in other words, is faster than Navigator.of()
, StatefulWidget
in other words.
But why do we have two different approaches to finding the target Element
? Why we can't just use dependOnInheritedWidgetOfExactType()
for both Navigator.of()
and DefaultTextStyle.of()
?
The answer is that Navigator.of()
and DefaultTextStyle.of()
have different purposes.
Navigator.of()
's approach is to find the State
of StatefulWidget
mainly for updating the state managed by the ancestor State
.
On the other hand, DefaultTextStyle.of()
's approach is to find the InheritedWidget
mainly for building UIs using values of the InheritedWidget
in build()
method.
Because build()
method is called whenever rebuilding is triggered, which means it is called frequently, we should optimize the cost of build()
method as much as possible. That's why DefaultTextStyle.of()
's approach is faster than Navigator.of()
's approach and independent from the depth of the widget tree.
However, we have some cons to use DefaultTextStyle.of()
's approach.
Because the references to the ancestor InheritedElement
are preserved in the map whose key is Type
, it can't preserve multiple InheritedElement
s with the same type. It only preserves the closest InheritedElement
with the same type.
On the other hand, Navigator.of()
's approach doesn't have that limitation. Because it tries to find the target Element
one by one using while
loop, we can choose the desired Element to find from the closest or the farthest Element
with the same type.
Anyways, both approaches require BuildContext
, which preserves the references to the ancestor Element
, to find the target Element
.
What is context.mounted
?
We sometimes need to check if the BuildContext
is mounted or not by calling context.mounted
. What is that?
As we discussed so far, BuildContext
is Element
corresponding to the widget. Element
will be disposed when the widget is no longer in the same position on the widget tree after rebuilding.
Usually, as the context
is an Element corresponding to our widget, we don't encounter the situation where the context
, or Element
, is disposed even though the operation declared in the corresponding widget is still running.
However, once we await a Future
, there happens a possibility that the corresponding Element
is disposed before the Future
is completed. In that case, it doesn't ensure the Element
is still alive at the following code running.
In addition, once the Element
is disposed, the references to its parent or children are also disposed, meaning BuildContext
's ability to access its ancestor or descendant is not there anymore.
That's why we have to check context.mounted
to ensure the Element
is still alive between awaitable tasks and any operation using BuildContext
.
The implementation of context.mounted
is so simple as follows:
@override
bool get mounted => _widget != null;
It just checks if the Element
has its corresponding widget or not.
BuildContext and WidgetRef
It's not a story of the Flutter framework itself, but I'd like to introduce one interesting fact that WidgetRef
of flutter_riverpod
package is exactly the same as BuildContext
.
_ConsumerState
, which is a subclass of State
calls build()
method of ConsumerWidget
as follows:
class _ConsumerState extends ConsumerState<ConsumerWidget> {
@override
Widget build(BuildContext context) {
return widget.build(context, ref);
}
}
We can see the second argument of build()
method ref
, which is WidgetRef
instance, and it is defined in ConsumerState
as follows:
abstract class ConsumerState<T extends ConsumerStatefulWidget>
extends State<T> {
late final WidgetRef ref = context as WidgetRef;
}
It's now clear that WidgetRef
is nothing but BuildContext
. Because the ConsumerStatefulElement
, which is a subclass of Element
defined in flutter_riverpod
package, implements WidgetRef
, this can be cast to WidgetRef
from BuildContext
or vice versa.
class ConsumerStatefulElement extends StatefulElement implements WidgetRef {
}
This fact tells us one important usage of ref
, which we have to check context.mounted
before using ref
after awaitable task.
await someTask();
if (context.mounted) { // need this check
ref.read(someProvider.notifier).someMethod();
}
Though context == ref
always returns true, this API design would be important for package users to avoid confusion and misuse of ref
. It's interesting and insightful considering when we make shared widgets, isn't it?
Conclusion
BuildContext
is often described as an object that preserves the "position" of the widget on the widget tree. However, once we dive into the implementation of BuildContext
and related components in the Flutter framework, we can find a lot of facts and fundamental mechanisms that make the framework work.
The knowledge of this article will NOT change your code directly, Navigator.of(context)
is still Navigator.of(context)
, but it will help you understand what is happening inside the method, and the understanding will help you to make better considerations about how you treat BuildContext
in your code.
BuildContext
is an interface for Element
. Element
is the most important component in the Flutter framework, which manages rebuilding and forms the widget tree by preserving the references to its parent and children. We have two approaches to communicating with Element
s in the widget tree, findAncestorStateOfType()
and dependOnInheritedWidgetOfExactType()
.
I'll keep writing other articles about the Flutter framework, and all of them will be based on the knowledge of this article.
That's it and thank you for reading!