Why every Flutter Dev should care about BuildContext
There has been a lot of discussion in the Flutter community recently surrounding the topics of abstraction and BuidContext
. This post aims to dispel the myth that you don't need BuildContext and discuss exactly why it's crucial for all Flutter devs to understand.
While the quest for abstraction and the purist’s assertion that all APIs should be simple by design are both noble pursuits, there is a point where too much abstraction becomes harmful and hinders the personal growth of new developers.
One of the most common complaints of developers who are new to Flutter is the general ambiguity around BuildContext and its importance to the framework. In this blog post, we will take a closer look at the importance of this class and how the framework uses it. Most importantly, we will look at ways developers can use this class to write more efficient Flutter applications.
What Is BuildContext?
Simply put, the BuildContext of a widget represents the widget's location in Flutter's Widget Tree.
Widget build(BuildContext context)=> MyAwesomeWidget();
You've probably written this line a thousand times in your Flutter application, but do you ever wonder where the context comes from or why it is necessary for each widget?
As you may recall, Flutter is made up of three main trees:
- Widget
- Element
- Render
If you’re not too familiar with Flutter’s architecture, read this article for a comprehensive explanation of the inner workings of these trees. For the purposes of this post, here is what you need to know:
Widgets
Widgets are immutable. They represent the configuration for RenderObjects. Flutter is optimized such that it can easily create and destroy widgets without any significant performance implications.
The same can’t be said for RenderObjects.
RenderObjects
RenderObjects, on the other hand, are mutable objects that do the heavy lifting of turning the configuration supplied from widgets into pixels users can see and interact with on the screen.
Unlike widgets that are cheap and can safely be created and destroyed without any significant performance implications, the same cannot be said for RenderObjects. For this reason, whenever the configuration of a widget changes, the framework looks at the change and updates the associated RenderObject instead of creating a new one each time.
Elements
In the middle of Widgets and RenderObjects sits Elements. These act as the glue between the immutable widget layer and the mutable render layer. As the configuration of a widget changes (for example, a user calls setState
which triggers a rebuild), the element looks at the incoming changes and tells the associated render object, "Hey! The user requested a change, it is time for you to update."
Bringing It Back to Our Code
Going back to the method signature at the start of the section, “BuildContext context
" provided by the widget’s build
method represents the underlying element for the widget. It serves as the bridge for changes in the widget world and updates in the rendering world.
When called, each widget you create has a context that becomes the widget's parent.
For example, consider the following code:
class MyAwesomeWidget extends StatelessWidget{
@override
Widget build(BuildContext context)
return Container(
child: Text("Hello there 👋"),
);
}
When our widget MyAwesomeWidget
is built, the context
defined in the build method becomes the parent of our Widget Tree. In simpler terms, the widgets returned by our build method live “under” the context of our MyAwesomeWidget
.
As a result of this behavior, you might have encountered some seemingly strange errors when trying to work with context.
Let's imagine a scenario where you had some code like the following:
See example: dartpad.dev/?id=60d1b93fde5ebb9a3207571881…In the example above, we have a simple widget containing some text and an InkWell
. When the user clicks on the text, we call the showBottomSheet
on the Scaffold to present a message.
Running the above code will throw the following error:
Debug service listening on ws://127.0.0.1:57355/YAQbr2NXIIg=/ws
Syncing files to device macOS...
======== Exception caught by gesture ===============================================================
The following assertion was thrown while handling a gesture:
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.
There are several ways to avoid this problem. The simplest is to use a Builder to get a context that is "under" the Scaffold. For an example of this, please see the documentation for Scaffold.of():
https:api.flutter.dev/flutter/material/Scaffold/…
A more efficient solution is to split your build function into several widgets. This introduces a new context from which you can obtain the Scaffold. In this solution, you would have an outer widget that creates the Scaffold populated by instances of your new inner widgets, and then in these inner widgets you would use Scaffold.of().
A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, then use the key.currentState property to obtain the ScaffoldState rather than using the Scaffold.of() function.
The context used was: MyAwesomeWidgetBroken
dependencies: [_InheritedTheme, _LocalizationsScope-[GlobalKey#777a2]]
When the exception was thrown, this was the stack:
#0 Scaffold.of (package:flutter/src/material/scaffold.dart:1785:5)
#1 MyAwesomeWidgetBroken.build.<anonymous closure> (package:git_feed/main_demo.dart:23:33)
#2 _InkResponseState._handleTap (package:flutter/src/material/ink_well.dart:989:21)
====================================================================================================
Let's examine this stack trace. It may look complicated but the error is actually in the first line. If it’s your first time running into an error like this, just take your time and read through each line. Flutter has been gradually improving the quality of error messages thrown by the framework. For common errors like this, the error includes background information on the error and steps users can take to address them.
The following assertion was thrown while handling a gesture:
Scaffold.of() called with a context that does not contain a Scaffold.
The framework is telling us that it cannot find the Scaffold widget using the context from our widget.
You might be wondering, "How is this possible? I can see the context in our build method".
class MyAwesomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: InkWell(
// What gives? Aren't we using a context here?
onTap: () => Scaffold.of(context)
.showBottomSheet((context) => SnackBarContent(),
),
child: Text(
'Tap me for a message',
style: Theme.of(context).textTheme.headline4,
),
),
),
);
}
}
Going back to what we learned earlier, the context provided by the builder of MyAwesomeWidget
will become the parent of the Widget Tree. As a result, the Scaffold widget lives under the context. Since Flutter uses the context to look up the tree, our Scaffold is missed.
If we look at where MyAwesomeWidget
is called, you can see there is no Scaffold acting as the parent of our widget.
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: MyAwesomeWidget(),
);
}
}
The solution is a simple one. We need a context containing a Scaffold. To solve this, we can introduce an additional context between the Scaffold, and where we attempt to retrieve the scaffold using Scaffold.of
.
class MyAwesomeWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Builder(builder: (newContext) {
return InkWell(
onTap: () => Scaffold.of(newContext).showBottomSheet(
(context) => SnackBarContent(),
),
child: Text(
'Tap me for a message',
style: Theme.of(context).textTheme.headline4,
),
);
}),
),
);
}
}
So far, we’ve only been using one widget for the sake of simplicity, but what’s important to remember is that the principles discussed for our single widget apply to every widget in Flutter. This means that in our simple example, every widget we use has its own context and subtree.
See example: dartpad.dev/?id=f4516a936cd1fd75bdb3e3a4e5…💡 It’s worth noting that the example above is intentionally simplistic. Since every widget defines its own context, an alternative (and more practical) solution would be to extract the body of the Scaffold to a contained Stateless/Stateful widget.
This does not mean that using a
Builder
is wrong. Like most things in programming, it depends on your use case and the size of your tree.
Why Is BuildContext Important?
There are many reasons why BuildContext is important. From locating widgets in the tree to interacting with RenderObjects, the context
makes all of these interactions possible.
It serves as the “bridge” between the widgets and rendering layer and is especially powerful for combining functionality or using information from one tree in another.
Lookups
By far, the most common use case for BuildContext in your everyday applications is for looking up and passing information around your app.
Whether you are using a state management solution like flutter_bloc
or provider
, every reputable state management library uses BuildContext
under the hood to efficiently propagate information down the tree.
At the core of this is the InheritedWidget
class. While we won't be covering the inner workings of this class in this blog post, we will go over the essential pieces.
Widget build(BuildContext context){
final theme = Theme.of(context);
return MyAwesomeWidget(color: theme.primaryColor);
}
In your day-to-day Flutter apps, developers frequently interact with InheritedWidgets
in one form or another. If you've ever used any of the .of(context)
methods in your applications, you are performing a lookup using an inherited widget.
Under the hood of most .of(context)
methods, you will find a call to dependOnInheritedWidgetOfExactType
. This is a crucial method in the framework. The sole responsibility of this method is to search the Widget Tree until it can find the nearest instance of the class specified by the generic type T
.
In our snippet above, our call to Theme.of(context)
will search our Widget Tree for the closest instance of the Theme
class.
In most applications, this could be the class created by your MaterialApp
, or for more complex applications, it can be a modified instance of your theme somewhere else in your application.
💡 If you’re using a nested
Theme
(or any context-dependent API in your application), can you guess what potential bug you may encounter?If “.of() called with a context that does not contain an XXXX” was on your bingo card, give yourself ten cool points 😎.
As we discussed in the previous section, the
context
provided by thebuild
method of widgets becomes the parent of that widget. When using a nested theme (or any context-dependent) in a shallow tree, you may have to wrap the widget access data from the API in a builder to avoid this issue.
Performance
Everyone’s favorite topic, performance. A well-structured app with clear responsibilities and data flow inevitably leads to better performance.
Generally, you don’t need to worry too much about Flutter performance. The engine will do the heavy lifting most of the time and all you need to care about are Widgets. But sometimes Flutter needs guidance and having a better fundamental understanding of how Flutter works will enable you to drive better architectural decisions.
To understand the importance of BuildContext in terms of performance, it’s important to know that the BuildContext passed in every build function is actually the corresponding Element wrapped in the BuildContext interface.
As previously stated, Elements are the glue between Widgets and RenderObjects, they retain the structure of the user interface or Widget Tree.
We know that widgets are immutable, which means, among other things, that they can’t remember their parent or child relationships with other widgets. After widgets are built they are held by the Element tree, which retains the logical structure of the user interface. The Element tree also holds the state objects associated with Stateful Widgets.
The use of Elements and the Element tree is what allows us, as Flutter developers, to quickly recreate our Widget Tree, without having to worry about performance.
Reusing elements is important for performance because elements own two critical pieces of data: the state for stateful widgets and the underlying RenderObjects. When the framework is able to reuse an element, the state for that logical part of the user interface is preserved and the layout information computed previously can be reused, often avoiding entire subtree walks. In fact, reusing elements are so valuable that Flutter supports non-local tree mutations that preserve state and layout information.
Flutter allows us to build and rebuild widgets freely, and inexpensively. However, rebuilding a widget does not necessarily equate to the engine rebuilding all of the render objects associated with that widget. The Element tree determines what can be reused in our current user interface.
Flutter is smart enough to not recreate the Elements and RenderObjects for parts of the Widget Tree that did not change during a particular build call. Sometimes, however, we need to guide Flutter to help it more optimally determine what parts of the tree can be reused - typically through the use of Keys.
The inner workings of Flutter is a deep topic, and unfortunately outside the scope of this article. Here are some good resources for further reading:
- Inside Flutter
- What are Widget, Elements and Render Objects
- The Mahogany Staircase - Flutter’s Layered Design
- Flutter’s Rendering Pipeline
But understanding BuildContext brings you one step closer to mastering the Flutter internals.
Crossing the Bridge: Interacting With RenderObjects
You may recall that BuildContext represents the underlying element between Widgets and RenderObjects. They act as an interface to discourage direct manipulation of Element objects.
As a result, developers can use the context
to interact with and access the associated RenderObject for each widget.
At first glance, this might not seem that important, but it becomes invaluable in rare situations where you need to access information on the RenderObject.
You might’ve already seen some examples of this being used and not even realized it.
In the example above, the context is used to look up the RenderObject and retrieve sizing information to construct the Rect used for the container transform.
This is just a single use-case where accessing the RenderObject becomes useful. In other applications, it can be used to calculate positioning for elements such as overlays and linked elements like custom drop-downs (this is a use-case we have in the Stream SDK).
Using BuildContext Effectively
If you are using one of the popular packages like provider
, chances are you are already using BuildContext effectively in your application.
If you ever feel like you are fighting the framework to perform basic tasks, then that’s a sign of a larger underlying problem in your application. In these cases, take a step back and look at the data flow in your app and the way methods are accessed.
Ask yourself:
- Am I using a nested navigator?
- Am I using the wrong context? Should my widget be moved higher in the tree?
- Is my widget too low in the tree?
These are just some of the questions you can ask yourself to help debug context issues. In most cases, the solution can be found by moving your widgets higher or lower in the tree.
For the rare circumstances where this is not the case, a GlobalKey
can be used to attach and access the state of widgets elsewhere in your app.
💡 Note: This should not be your first choice as
GlobalKeys
can be expensive if used incorrectly.
Let’s Recap
Wow, we covered a lot in this piece. Let’s do a quick recap:
Flutter is made up of three distinct trees:
- Widget
- Element
- Rendering
The context
parameter we all know and love represents the underlying element that ties the Widget and Rendering layer together. Unlike widgets that are immutable and stateless, elements are long-lived classes that act as a coordinator between changes on the widget layer and render layer.
The parameter supplied by your widget’s build
function becomes the parent of **the widget which can lead to familiar errors like “Widget not found in a given context” when trying to access or call context-dependent APIs.
Making effective use of the context can help reduce unnecessary builds in your application and go a long way in helping you write cleaner and more effective Flutter code.
Do you struggle with context
in your application? Tweet me @Nash0x7E2 and let’s talk about it.
— Nash