If you are a Flutter developer, it is almost impossible not to encounter BuildContext. When Flutter builds a widget, it supplies a BuildContext to its build method. Whenever you try to access the theme of your Flutter app, you use Theme.of(context). But what exactly is context?
BuildContext is a handle to the location of a widget in the widget tree. You can think of it as the widget’s address within your app. It is not the widget or its state. That said, there are a few more things about BuildContext that are essential for our understanding.
Every BuildContext represents one specific location in the tree. The context passed to a build method belongs to the widget whose method is running, while the widgets returned by that method receive their own contexts.
A BuildContext can remain valid across many rebuilds. However, it becomes invalid when its associated element is unmounted. This is why Flutter warns us about using a context across asynchronous gaps: the widget may have been removed from the tree while the asynchronous operation was running. After an asynchronous gap, check context.mounted before using the context.
Let's further analyze BuildContext through a concrete example. As usual, create a Flutter project, navigate to lib/main.dart, and replace its contents with the following:
import 'package:flutter/material.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Theme(
data: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
textTheme: TextTheme(bodyMedium: TextStyle(color: Colors.grey)),
),
child: const MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
FirstSection(),
SecondSection(),
],
),
),
);
}
}
class FirstSection extends StatelessWidget {
const FirstSection({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
spacing: 8,
children: [
FilledButton(onPressed: () {}, child: Text('I am the first section button!')),
Text('I am the first section body text!', style: theme.textTheme.bodyMedium),
],
);
}
}
class SecondSection extends StatelessWidget {
const SecondSection({super.key});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Column(
spacing: 8,
children: [
FilledButton(onPressed: () {}, child: Text('I am the second section button!')),
Text('I am the second section body text!', style: theme.textTheme.bodyMedium),
],
);
}
}
We have a fairly simple screen with a column that has two sections. This is what our widget tree looks like at this stage:
Notice how our home page is wrapped with a
Themewidget.
Now let's take a look at the first line in the build method of both sections:
final theme = Theme.of(context);
In both sections, we tell Flutter to use context as the starting point and walk up the widget tree until it finds a Theme. In both cases, Theme.of(context) returns the ThemeData provided by the Theme widget just above MyHomePage. But let's make a change to make things interesting:
Wrap the FirstSection widget in a Theme widget and pass these values to it:
Theme(
data: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.yellow),
textTheme: TextTheme(bodyMedium: TextStyle(color: Colors.cyan)),
),
child: FirstSection(),
),
This is what the widget tree should look like after the change:
Now perform a hot reload and see how the app changes. The first section now uses a different theme. This is because when Flutter starts from the FirstSection widget and walks upwards, the first Theme widget it sees is different from the one used by the SecondSection widget. In the SecondSection widget, Theme.of(context) sees the Theme that is right above the MyHomePage widget, and returns its ThemeData. However, in the FirstSection widget, it returns the ThemeData of the Theme widget that is right above itself. The BuildContext passed to Theme.of provides the starting location for each lookup, allowing it to select the nearest enclosing Theme.
Let's take a look at another example that might save you from future headaches. This example is very important and will help solidify your understanding of BuildContext. Replace the contents of lib/main.dart with the following:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: FilledButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Hi!'))),
child: Text('Press me!'),
),
),
),
);
}
}
This is the widget tree:
Run the app and press the button. You will get an error that says: No ScaffoldMessenger widget found. It might seem odd at first. After all, the FilledButton is inside the Scaffold, so why is it not working?
The context passed to ScaffoldMessenger.of(context) belongs to the MyApp widget. Although the button appears inside the Scaffold, its callback captures MyApp's context, which is above the ScaffoldMessenger created by MaterialApp. Therefore, ScaffoldMessenger.of(context) cannot find a ScaffoldMessenger ancestor. To fix this, we need a new context from lower in the widget tree.
You can use the Builder widget like so:
Builder(
builder: (context) {
return FilledButton(
onPressed: () => ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text('Hi!'))),
child: Text('Press me!'),
);
},
)
Tip: If you are using VS Code, simply place the cursor on the
FilledButtontext, press the lightbulb icon, and press "Wrap with Builder".
Builder introduces a new BuildContext at its own position in the tree. The context passed to its callback belongs to the Builder widget and can be used by the FilledButton returned from that callback. With this change, our widget tree looks like this:
Perform a hot reload and press the button again. This time, Flutter successfully shows the snackbar. The context passed to ScaffoldMessenger.of now belongs to the Builder, which is below the ScaffoldMessenger created by MaterialApp. The lookup therefore finds the nearest enclosing ScaffoldMessenger, which displays the snackbar through its registered Scaffold.
BuildContext might seem confusing at first, but it all becomes easier when you realize it is just a widget's address within your app. When a context-based lookup fails, check where the context comes from and whether the widget you need encloses that location. If necessary, use Builder to obtain a context from lower in the tree, and always check if a context is still mounted before using it after an asynchronous gap.




Top comments (0)