All I Know about BuildContext

All I Know about BuildContext

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().

A picture how Navigator.of(context) tries to find NavigatorState

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 InheritedElements, which are associated Elements of InheritedWidgets, 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 InheritedElements 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 Elements 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!