BuildContext, Widget Tree and Provider

Soojeong Lee
5 min readFeb 17, 2024

--

If you’ve used some form of build() method to build widgets in Flutter, or have been using Consumer<T> with Provider, you’ve probably already encountered BuildContext.

However, you might not really understand what they are in terms of widget trees. This article will discuss what BuildContext is, and how to properly approach this in terms of Consumer of the Provider library.

What is BuildContext?

To keep it simple, BuildContext “handles the location of a widget in a widget tree”. A widget tree holds the entire structure of how each widget is layered and connected. Then, this widget tree is translated into an element tree during build. These elements are then depicted through the layout, paint, composition, then finally rasterization phase.

If you would like to understand how rasterization occurs through Flutter’s engine, checkout my article here.

Widget Tree and Element Tree from Flutter

In this widget tree, where each widget is located is referenced through the BuildContext. If you’re building a widget with build(), this is passed as a parameter.

What is important to note, is that a widget holds a BuildContext, which becomes the parent of the widget that is returned by StatelessWidget.build() or State.build(). So a parent widget A’s BuildContext is passed as a parameter, and the child widget B is returned from the function above.

This seems pretty straightforward, but is it?

Getting a Bit Complex

What you should note is that the BuildContext of the widget of the build method is different from the BuildContext of the widget returned.

So the BuildContext of the parent and the ancestors are different, which is obvious considering the role of a BuildContext is to handle the location of the widget in the tree.

This is a simple code example, similar to the one in the BuildContext documentation.

// Class ExampleScreen 

@override
Widget build(BuildContext contextA) {
return Scaffold(
body: Builder(
builder: (BuildContext contextB) {
// TextButton becomes child of Scaffold through Builder
return TextButton(
child: const Text('BUTTON'),
onPressed: () {
// What would happen here?
Scaffold.of(contextB).showBottomSheet(
(BuildContext contextC) {
return Container();
},
);
},
);
},
)
);
}
Widget Tree analyzed with Flutter Inspector

After analyzing the widget tree with Flutter inspector, we can see this result. Scaffold -> Builder -> TextButton is pretty evident from above.

But what about Scaffold.of(context).showBottomSheet and its returned Container? We can see from above that it has become the child of Scaffold.

What happens if we pass the context of the first BuildContext, AKA contextA?

// Class Examplecreen 

@override
Widget build(BuildContext contextA) {
return Scaffold(
body: Builder(
builder: (BuildContext contextB) {
return TextButton(
child: const Text('BUTTON'),
onPressed: () {
// Passing contextA?
Scaffold.of(contextA).showBottomSheet(
(BuildContext contextC) {
return Container();
},
);
},
);
},
)
);
}

// Scaffold.of() called with a context that does not contain a Scaffold.

Oops! Nothing happens! This is because contextA returns a Scaffold. The parent widget’s contextA results in a child Scaffold, which then holds contextB . So contextA would not contain a Scaffold yet!

Until it finds a context that contains a Scaffold, Scaffold.of will continue to travel up the widget tree to find an ancestor that does.

I made a new structure in which an ancestor Scaffold exists (HomeScreen is the parent that holds ExampleScreen). In this case, passing contextA will not result in an error, since Scaffold.of is able to travel up and finally find a context that holds a Scaffold.

No Error for When Ancestor Holds Scaffold

Creating a Provider

First of all, a Provider needs to exist in the widget tree if you are to use it. A Provider is in essence, an inherited widget.

Let’s say you will use some form of ChangeNotifierProvider. In most cases, you would create this ChangeNotifierProvider, and its child will be the scope you want to use the Provider for. It can be a screen, or it can be the entire App itself.

Here, I’m using AppProvider that will be used to control the entire global setting of the application, such as language or theme. Therefore, I’ve placed it above the widget tree so it can access the entire scope.

void main() {
runApp(
ChangeNotifierProvider(
create: (_) => AppProvider(),
child: const MyApp(),
),
);
}
Widget Tree Structure

You can see that the ChangeNotifierProvider has become the parent widget of MyApp. When using Provider.of(context), the BuildContext of a widget that has the Provider must be passed as a parameter so that the Provider can be found.

Consumer and Provider

So what does Consumer exactly do? According to the documentation, “it just calls Provider.of in a new widget, and delegates its build implementation to builder.”

Take a look at this example here. Let’s say I’m creating a ChangeNotifierProvider in the ExampleScreen, not above the application, then immediately trying to get the current ThemeMode from my AppProvider.

// Class ExampleScreen

@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AppProvider(),
child: Text(Provider.of<AppProvider>(context).themeMode.toString()),
);
}

// Error: Could not find the correct Provider<AppProvider> above this ExampleScreen Widget

Ooops! Nothing happens (again)! The context being passed as a parameter is from the parent of the ChangeNotifierProvider, therefore Provider.of has failed to find the Provider since that context does not contain a Provider.

Consumer solves this problem, by calling Provider.of with its own BuildContext instead of trying to use the wrong ancestor context.

// Class ExampleScreen

Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => AppProvider(),
child: Consumer<AppProvider>(
builder: (_, appProvider, __) => Text(appProvider.themeMode.toString(),
),
),
);
}
Correctly Shown Widget Tree

Here, you can see that Consumer has become the child of ChangeNotifierProvider, and has successfully used the BuildContext of the ChangeNotifierProvider to correctly find it in the widget tree.

General Rule of Thumb

To understand the structure of how each of your widgets are located, make sure to use the Flutter Inspector. See how the BuildContext is passed from parent to child, to correctly finding the right context.

Visualize the widget tree, see how each widget is connected structurally, and see how each widget is created or destroyed during its lifecycle.

Happy Coding!

--

--