Flutter state management is a broad subject and it can be hard to make sense of it all.
In this tutorial, I'll help you understand the basics by explaining what is state and the difference between local and shared/global state.
And I'll give you an overview of the built-in widgets that you can use to manage state in Flutter, along with links to the best resources from the official Flutter documentation.
You can go quite far and build non-trivial applications with the built-in Flutter state management widgets. It's worth learning how they work before moving to more complex solutions.
Ready? Let's go!
What is state?
Flutter apps are built with a declarative programming model.
This means that widgets define their UI by overriding the build()
method, which is a function that converts state to UI:
UI = f(state)
In this context, the Flutter documentation defines state as:
whatever data you need in order to rebuild your UI at any moment in time
We could also say that the UI is all about how your application looks like. And the state is about how your application behaves.
In principle, this is a simple concept:
state => UI
What is complex is that there are many different ways of transforming state into UI, with different pros and cons.
Not only that, but there are two different kinds of state: local state and global state.
Local vs Global State
As we know, we can compose Flutter widgets into a widget-tree that represents our application's UI:
Sometimes you have state that can be self-contained inside a single widget. That state is known as local or ephemeral state and Flutter offers some built-in tools such as setState
and StatefulWidget
to deal with this.
If you have some simple state that only affects the behaviour of a single widget, StatefulWidget
is all you need. This video from the official Flutter channel explains how to use it:
On the other hand, when state needs to be shared across multiple widgets or even the entire app, you're dealing with shared / global app state.
Sharing state across multiple widgets is where things get interesting and there are many different techniques to do this. So let's review what Flutter offers out-of-the-box:
InheritedWidget
You can use Flutter's built-in InheritedWidget
to share state across multiple widgets, as explained in this video:
InheritedWidget
is all about making some data or state available to multiple widgets via scoped access. For example, inside your widgets you can call Navigator.of(context)
to access the main Navigator
and this uses InheritedWidget
under the hood:
Implementing your own
InheritedWidget
subclasses is not easy and quite error-prone. For this reason, the provider package was introduced.
ValueNotifier & ChangeNotifier
Flutter includes a ValueNotifier
class as a way to store your state outside your widgets. You can use it with a ValueListenableBuilder
widget to update the UI when the state changes. This is best explained here:
Here's an example showing how to use ValueNotifier
and ValueListenableBuilder
together:
final valueNotifier = ValueNotifier<int>(42);
ValueListenableBuilder<int>(
valueListenable = valueNotifier,
// called when the value changes
builder: (context, value, _) {
return Text('Value is $value')
}
);
Flutter also comes with ChangeNotifier
, which is the "cousin" of ValueNotifier
. For a practical explanation of how to use it, see this page.
FutureBuilder & StreamBuilder
Many asynchronous APIs use Futures and Streams to notify your app when new data is available.
Their difference is that a Future produces a single asynchronous value, while a Stream can produce many asynchronous values over time.
Both Futures and Streams are part of the Dart SDK. My Complete Dart Language course covers them in great detail.
You can use Flutter's FutureBuilder
widget to decide what widget to show depending on the state of the Future (loading, data, or error):
Similarly, use Flutter's StreamBuilder
widget to rebuild your UI when a stream emits new data:
When loading asynchronous data from a Stream-based API, it's good practice to check for these UI states: data, no data, error, loading.
StreamBuilder
supports this but it is quite clunky to check for data or errors inside the builder's snapshot
:
final stream = Stream.fromIterable([21, 42]);
StreamBuilder<int>(
stream: stream,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.active) {
if (snapshot.hasData) {
return MyWidget(snapshot.data); // data
} else if (snapshot.hasError) {
return MyErrorWidget(snapshot.error); // error
} else {
return Text('No data'); // no data
}
} else {
return CircularProgressIndicator(); // loading
}
}
)
3rd party packages such as flutter_bloc, Provider, and Riverpod offer alternatives to
StreamBuilder
that are much easier to use.
Using Flutter's built-in widgets
You can go quite far with the built-in StatelessWidget
, StatefulWidget
, and InheritedWidget
classes. You can use ChangeNotifier
or ValueNotifier
to manage your state, or FutureBuilder
and StreamBuilder
when reading data from asynchronous APIs.
This official guide shows how to put things together using ChangeNotifier
and ChangeNotifierProvider
to build a simple shopping cart application:
Wrap Up
The built-in widgets we covered are at the basis of state management in Flutter. Over 40 state management packages now exist, but they all build on the same principles and foundations.
So take the time to understand the basics by exploring the resources I shared.
And once you're ready to dig deeper, I recommend learning about Riverpod, a powerful reactive caching and data-binding framework that was born as an evolution of the Provider package.
I've covered Riverpod extensively on this site, and the best place to start is here:
Happy coding!