Understanding Flutter's BuildContext
In Material Design, when we want to give some subtle feedback to the user, we may be inclined to use a SnackBar:
If we are using a Scaffold
in our screens, we can easily show it by using the showSnackBar
of the context as follows:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: MaterialButton(
child: Text(
"SnackBar",
style: TextStyle(color: Colors.white),
),
color: Colors.red,
onPressed: () => Scaffold.of(context).showSnackBar(
SnackBar(
content: Text("Oh! You clicked me!"),
),
),
),
),
);
}
}
But this will result in the following error:
flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following assertion was thrown while handling a gesture:
flutter: Scaffold.of() called with a context that does not contain a Scaffold.
flutter: No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). This
flutter: usually happens when the context provided is from the same StatefulWidget as that whose build
flutter: function actually creates the Scaffold widget being sought.
flutter: There are several ways to avoid this problem. The simplest is to use a Builder to get a context that
flutter: is "under" the Scaffold. For an example of this, please see the documentation for Scaffold.of():
flutter: https://api.flutter.dev/flutter/material/Scaffold/of.html
flutter: A more efficient solution is to split your build function into several widgets. This introduces a
flutter: new context from which you can obtain the Scaffold. In this solution, you would have an outer widget
flutter: that creates the Scaffold populated by instances of your new inner widgets, and then in these inner
flutter: widgets you would use Scaffold.of().
flutter: A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, then use the
flutter: key.currentState property to obtain the ScaffoldState rather than using the Scaffold.of() function.
════════════════════════════════════════════════════════════════════════════════════════════════════
We now have two possible solutions:
- Either we don’t read the error message and try to fix it any way;
- Or we read the error message and try to fix it and understand why this happens.
Following the first route, we may be inclined to access the showSnackbar
method, which resides in the ScaffoldState
, with a GlobalKey
:
class MyHomePage extends StatelessWidget {
// The GlobalKey will allow us to access the Scaffold's State
final GlobalKey<ScaffoldState> _key = GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context) {
return Scaffold(
// We add the key to the Scaffold so we can access it later
key: _key,
body: Center(
child: MaterialButton(
child: Text(
"SnackBar",
style: TextStyle(color: Colors.white),
),
color: Colors.red,
// We now can access the currentState via the GlobalKey
onPressed: () => _key.currentState.showSnackBar(
SnackBar(
content: Text("Oh! You clicked me!"),
),
),
),
),
);
}
}
Though this works, the GlobalKey
documentation page states that: “Global keys are relatively expensive”, so we should avoid using them when we can find another solution.
The Tale of Two BuildContexts
So what’s in the error message that was displayed above?
/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./
What is happening is that both the MaterialButton
and the Scaffold
are being built by the same BuildContext
as we can see in the following diagram:
Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue
Inspecting the Scaffold.of(context)
method:
static ScaffoldState of(BuildContext context, { bool nullOk = false }) {
final ScaffoldState result = context.findAncestorStateOfType<ScaffoldState>();
if (nullOk || result != null)
return result;
throw FlutterError.fromParts(/* ... */);
}
The findAncestorStateOfType
method describes in its name what its purpose is - to find a Widget
that is above in the Widget tree whose State’s matches the ScaffoldState
. This can be done since the BuildContext
has the notion of parent
, and with it we can scale up the tree. To understand how it works, we look at the Element
class, which is responsible for instantiating Widgets and implements BuildContext
:
T findAncestorStateOfType<T extends State<StatefulWidget>>() {
assert(_debugCheckStateIsActiveForAncestorLookup());
Element ancestor = _parent;
while (ancestor != null) {
if (ancestor is StatefulElement && ancestor.state is T)
break;
ancestor = ancestor._parent;
}
final StatefulElement statefulAncestor = ancestor;
return statefulAncestor?.state;
}
Since each Element
is aware of its _parent
widget, we can navigate up the tree and do a type check to see if we have found the ancestor we were looking for. If we have, then we can return it’s state via statefulAncestor.state
.
Looking at our diagram, we can clearly see now that if the MaterialButton
is at the same level that the Scaffold
then when the findAncestorStateOfType
method is called, the ScaffoldState
will not be found. So what can we do to solve it?
If we want to separate our UI elements into several classes, one way we can do it is to create a separate StatelessWidget
which will build the MaterialButton
separately:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: CustomButton(),
),
);
}
}
/// We separate the UI by creating a new StatelessWidget
class CustomButton extends StatelessWidget {
/// This build method will create a new `BuildContext` that will have as an ancestor the `Scaffold`
@override
Widget build(BuildContext context) {
return MaterialButton(
child: Text(
"SnackBar",
style: TextStyle(color: Colors.white),
),
color: Colors.red,
// We can now safely find the ancestor `Scaffold` via the `BuildContext`
onPressed: () => Scaffold.of(context).showSnackBar(
SnackBar(
content: Text("Oh! You clicked me!"),
),
),
);
}
}
Which can be translated in the following diagram:
Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue
When the Button
calls tries to find an ancestor with the CustomButton’s BuildContext
, it will find the Scaffold
as a direct ancestor, so the findAncestorStateOfType
find a ScaffoldState
which we can access to call the showSnackBar
method.
But what if we want to keep it all in the same StatelessWidget
? That’s where the Builder Widget comes in. It provides us with a WidgetBuilder
callback that provide us a new BuildContext for we to use:
class MyHomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
// The `Builder` widget will provide a `builder` method with its own `BuildContext` so that we can access any ancestor above it with this new context
body: Builder(
builder: (secondContext) => Center(
child: MaterialButton(
child: Text(
"SnackBar",
style: TextStyle(color: Colors.white),
),
color: Colors.red,
// Using the `Builder` `BuildContext`, named `secondContext` we can reach the `Scaffold` ancestor.
onPressed: () => Scaffold.of(secondContext).showSnackBar(
SnackBar(
content: Text("Oh! You clicked me!"),
),
),
),
),
),
);
}
}
This will create exactly the same Widget
-BuildContext
graph that we have seen above, with the main difference that now we have it all inside the same class.
Widget-Context Diagram; BuildContext is represented in yellow, Widgets are represented in Blue
Why should we take care of our BuildContext
We now know that the BuildContext
can be used to locate our widget and other widgets in the tree. But the question is - why should we care?
Let’s assume that we created an initial screen in which a Scaffold
accepts a StatelessWidget
as a body. As with the previous section, we will want to show a SnackBar
with some text.
We also decide to abstract the onPressed
method, but since we don’t have access to the BuildContext
in a StatelessWidget
, we cache it:
class CachedContextPage extends StatelessWidget {
/// The cached instance of the BuildContext
BuildContext _context;
/// Shows a snackbar with the cached BuildContext
void showSnackbar() {
Scaffold.of(_context).showSnackBar(
SnackBar(
content: Text(
"Yay!",
),
),
);
}
@override
Widget build(BuildContext context) {
print("Called build");
// Caching the context when this Widget is first built
if (_context == null) {
_context = context;
}
return Center(
child: RaisedButton(
color: Colors.red,
child: Text(
"Show Snackbar",
style: TextStyle(color: Colors.white),
),
onPressed: () => showSnackbar()),
);
}
}
Though this works as intended, we may ask: what happens if the Widget tree changes and it no longer has a Scaffold Ancestor?
Let’s look at the following example: we have two buttons in the screen:
- The first one shows a
SnackBar
- The second one changes the page root Widget from a
Scaffold
to aCupertinoPageScaffold
Example app showing SnackBar and changing the root Scaffold from Material to Cupertino
class MyHomePage extends StatefulWidget {
@override
_MyDeprecatedContextPageState createState() =>
_MyDeprecatedContextPageState();
}
class _My MyHomePageState extends State<MyHomePage> {
/// The child Widget
Widget _child;
bool _isMaterial = true;
@override
void initState() {
super.initState();
_child = PageBody();
}
@override
Widget build(BuildContext context) {
/// We check if the `isMaterial` flag is true to either return a `Scaffold` or a `CupertinoPageScaffold` as the root Widget
if (_isMaterial) {
return _getMaterialScaffold(_child);
}
return _getCupertinoScaffold(_child);
}
Widget _getMaterialScaffold(Widget child) => Scaffold(
appBar: AppBar(
title: Text("This is a text"),
),
body: child,
);
Widget _getCupertinoScaffold(Widget child) => CupertinoPageScaffold(
child: child,
navigationBar: CupertinoNavigationBar(
backgroundColor: Colors.red,
middle: Column(
children: <Widget>[
Icon(Icons.warning),
Text(
"Changed Theme!",
style: TextStyle(color: Colors.white),
)
],
),
),
);
/// Public method to change the current root widget
void changeType() {
setState(() => _isMaterial = !_isMaterial);
}
}
This Widget exposes the changeType
method that will force a rebuild with the different style.
The PageBody
will contain the two buttons and the methods to access both the Scaffold
, to show a SnackBar
, and the MyDeprecatedContextPage
so that we can access the public method changeType
. As with before, since it’s a StatelessWidget
, we opt to cache the BuildContext
class LeakedPageBody extends StatelessWidget {
BuildContext _context;
void showSnackbar() {
Scaffold.of(_context).showSnackBar(
SnackBar(
content: Text(
"Yay!",
),
),
);
}
void changePageType() {
_context
.findAncestorStateOfType<_MyHomePageState>()
.changeType();
}
@override
Widget build(BuildContext context) {
print("Called build");
// Caching the context
if (_context == null) {
_context = context;
}
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
RaisedButton(
color: Colors.red,
child: Text(
"Show Snackbar",
style: TextStyle(color: Colors.white),
),
onPressed: () => showSnackbar()),
SizedBox(
height: 20,
),
RaisedButton(
color: Colors.blue,
child: Text(
"Change Style",
style: TextStyle(color: Colors.white),
),
onPressed: () => changePageType()),
],
);
}
}
We can click on the “Show Snackbar” button once, and it shows the SnackBar
correctly. However, when we click the Change Style
button, the root widget of MyHomePage
will be changed to a CupertinoPageScaffold
that cannot be accessed via the Scaffold.of(_context)
method.
As predicted, when clicking again in the Show Snackbar
button again, the error that we have encountered before is thrown:
flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following assertion was thrown while handling a gesture:
flutter: Scaffold.of() called with a context that does not contain a Scaffold.
flutter: No Scaffold ancestor could be found starting from the context that was passed to Scaffold.of(). This
flutter: usually happens when the context provided is from the same StatefulWidget as that whose build
flutter: function actually creates the Scaffold widget being sought.
flutter: There are several ways to avoid this problem. The simplest is to use a Builder to get a context that
flutter: is "under" the Scaffold. For an example of this, please see the documentation for Scaffold.of():
flutter: https://api.flutter.dev/flutter/material/Scaffold/of.html
flutter: A more efficient solution is to split your build function into several widgets. This introduces a
flutter: new context from which you can obtain the Scaffold. In this solution, you would have an outer widget
flutter: that creates the Scaffold populated by instances of your new inner widgets, and then in these inner
flutter: widgets you would use Scaffold.of().
flutter: A less elegant but more expedient solution is assign a GlobalKey to the Scaffold, then use the
flutter: key.currentState property to obtain the ScaffoldState rather than using the Scaffold.of() function.
This does not surprise us, since we have seen that the Scaffold.of()
method uses the findAncestorStateOfType
that now will not find a ScaffoldState
in the widget tree.
What’s more interesting is that if we click again in the Change Type
button, it will not work and in turn it throws the following error:
flutter: ══╡ EXCEPTION CAUGHT BY GESTURE ╞═══════════════════════════════════════════════════════════════════
flutter: The following assertion was thrown while handling a gesture:
flutter: Looking up a deactivated widget's ancestor is unsafe.
flutter: At this point the state of the widget's element tree is no longer stable.
flutter: To safely refer to a widget's ancestor in its dispose() method, save a reference to the ancestor by
flutter: calling dependOnInheritedWidgetOfExactType() in the widget's didChangeDependencies() method.
As we can see from the error message, when we use the cached BuildContext
it is now referencing a deactivated widget.
Looking at the findAncestorStateOfType
method again, we verify that the in the first line there is a call to:
assert(_debugCheckStateIsActiveForAncestorLookup());
This will in turn verify if the Element
is in active state or not:
bool _debugCheckStateIsActiveForAncestorLookup() {
assert(() {
if (_debugLifecycleState != _ElementLifecycle.active) {
throw FlutterError.fromParts(/* */);
}
return true;
}());
return true;
}
This means that, while the parent was rebuilding, the cached BuildContext
was marked as not active, and we will no longer be able to look up ancestors in the tree. To learn more about how Flutter builds its UI elements, specifically Widgets, Elements and Render Objects, we can check the talk by Mark Sullivan and Andrew Fitz - “How Flutter renders Widgets” to understand how this works.
The final question is: how can we solve it? And, as we may guess, the solution is to always use the BuildContext
provided by the build
method:
class PageBody extends StatelessWidget {
void showSnackbar(BuildContext context) {
Scaffold.of(context).showSnackBar(
/* */
);
}
void changePage(BuildContext context) {
context
.findAncestorStateOfType<_MyDeprecatedContextPageState>()
.changeType();
}
@override
Widget build(BuildContext context) {
return Column(
/* */
children: <Widget>[
RaisedButton(
/* */
// We now pass the `BuildContext` from the `build` method directly to the method
onPressed: () => showSnackbar(context)),
SizedBox(
height: 20,
),
RaisedButton(
/* */
// We now pass the `BuildContext` from the `build` method directly to the method
onPressed: () => changePage(context)),
],
);
}
}
And this way, when the root widget changes from a Scaffold
to a CuppertinoPageScaffold
, the build
method of the PageBody
is called again to provide it with the new BuildContext
that can then be used in each RaisedButton
callback.
Conclusion
In this article we’ve seen why we should care about our BuildContext:
- It can be used to locate widgets that are above in the Widget tree;
- We have to be careful about which
BuildContext
we are using, since we may be looking at an ancestor that is in the same level, hierarchically; - It is bound to the
Element
’s lifecycle, which means that when using a cachedBuildContext
we may not know when it has been deactivated
There is still much to be learned about BuildContext
, specifically how we can depend on an InheritedWidget
and why updating it forces our Widget to rebuild. But that will be a topic for a future article.
Want to get the latest articles and news? Subscribe to the newsletter here 👇
And for other articles, check the rest of the blog! Blog - Gonçalo Palma