State Management in Flutter.
Samuel Wahome
Posted on January 7, 2022
Managing state in an application is one of the most important and necessary processes in the life cycle of an application. The main job of a UI is to represent state. According to the official Flutter documentation:
In the broadest possible sense, the state of an app is everything that exists in memory when the app is running. This includes the app's assets, all the variables that the Flutter framework keeps about the UI, animation state, textures, fonts, and so on.
From this definition, we can then conclude that the pattern that programmatically establishes how to track changes and how to broadcast details about states to the rest of your app is known as state management.
The two state types that are normally considered are ephemeral state(also known as UI state or local state)and app state(sometimes also called shared state):
-
Ephemeral state is the state you can neatly contain in a single widget. Ephemeral state is used when no other component in the widget tree needs to access a widget's data. Examples include whether a
TabBarView
tab is selected or when tracking the current progress of a complex animation. In other words, there is no need to use state management techniques (ScopedModel, Redux, Provider, etc.) on this kind of state. All you need is aStatefulWidget
. - State that is not ephemeral, that you want to share across many parts of your app, and that you want to keep between user sessions, is what is called application state. Application state when other parts of your app need to access a widget's state data. One example is an image that changes over time, like an icon for the current weather. Another example is when accessing login info that may be required in more than one section of your Flutter application.
Now that we've gotten the basic definitions out of the way, let's get to work exploring the various options we have at our disposal to manage the application state.
Managing state in a Flutter application.
1. InheritedWidget.
According to the official documentation:
InheritedWidget class is a base class for widgets that efficiently propagate information down the tree.
It's the basis for a lot of other state management widgets. If you create a class that extends InheritedWidget
and give it some data, any child widget can access that data by calling context.dependOnInheritedWidgetOfExactType<class>()
.
Inherited widgets, when referenced in this way, will cause the consumer to rebuild when the inherited widget itself changes state.
class SampleWidget extends InheritedWidget {
final Sample sample;
SampleWidget(Key? key, required this.sample, required Widget child}) :
super(key: key, child: child);
@override
bool updateShouldNotify(SampleWidget oldWidget) => sample!= oldWidget.sample;
static SampleWidget of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<SampleWidget>()!;
}
One can then extract data from that widget. Since that's such a long method name to call, the convention is to create an of()
method. Then a child widget, for example, a text field that displays the sample text, can just use:
SampleWidget sampleWidget = SampleWidget.of(context);
print(sampleWidget.sample.text);
An advantage of using InheritedWidget
is it's a built-in widget so you don't need to worry about using external packages.
A disadvantage of using InheritedWidget
is that the value of a Sample can't change unless you rebuild the whole widget tree because InheritedWidget
is immutable. So, if you want to change the displayed sample text, you'll have to rebuild the whole SampleWidget
.
2. Provider.
According to the official documentation:
Provider is a wrapper around InheritedWidget to make them easier to use and more reusable.
Provider
was designed by Remi Rousselet to wrap around InheritedWidget
, simplifying it. In short, Provider is a set of classes that simplifies building a state management solution on top of InheritedWidget
.
Provider has several commonly used classes such as: ChangeNotifierProvider
, Consumer
, FutureProvider
, MultiProvider
, StreamProvider
, etc.
ChangeNotifier
ChangeNotifier
is a class that adds and removes listeners, then notifies those listeners of any changes. You usually extend the class for models so you can send notifications when your model changes. When something in the model changes, you call notifyListeners()
and whoever is listening can use the newly changed model to redraw a piece of UI, for example.
ChangeNotifierProvider
ChangeNotifierProvider
is a widget that wraps a class, implementing ChangeNotifier
and uses the child widget for display. When changes are broadcast, the widget rebuilds its tree.
ChangeNotifierProvider(
create: (context) => SampleModel(),
child: <widget>,
);
Consumer
Consumer
is a widget that listens for changes in a class that implements ChangeNotifier
, then rebuilds the widgets below itself when it finds any. When building your widget tree, try to put a Consumer
as deep as possible in the UI hierarchy, so updates don't recreate the whole widget tree.
Consumer<SampleModel>(
builder: (context, model, child) {
return Text('Hello ${model.text}');
}
);
If you only need access to the model and don't need notifications when the data changes, use Provider.of
, as shown below:
Provider.of<SampleModel>(context, listen: false).<method name>
listen: false
indicates you don't want notifications for any updates. This parameter is required to use Provider.of()
inside initState()
.
FutureProvider
FutureProvider
works like other providers and uses the required create
parameter that returns a Future
.
FutureProvider(
initialData: null,
create: (context) => createFuture(),
child: <widget>,
);
Future<SampleModel> createFuture() async {
return Future.value(SampleModel());
}
A Future
is handy when a value is not readily available but will be in the future. Examples include calls that request data from the internet or asynchronously read data from a database or network.
MultiProvider
What if you need more than one provider? You could nest them, but it'd get messy, making them hard to read and maintain. Instead, use MultiProvider
to create a list of providers and a single child
:
MultiProvider(
providers: [
Provider<SampleModel>(create: (_) => Model()),
Provider<SampleDatabase>(create: (_) => Database()),
],
child: <widget>
);
StreamProvider
Provider also has a provider that's specifically for streams and works the same way as FutureProvider
. Stream providers are handy when data comes in via streams and values change over time like, for example, when you're monitoring the connectivity of a device, or listening to active changes from a network.
3. Riverpod.
According to the official documentation:
Rivepod can be considered as a rewrite of provider to make improvements that would be otherwise impossible.
Riverpod was written by Provider's author, Remi Rousselet, to address some of Provider's weaknesses. In fact, you'd be surprised to find out that Riverpod is in fact an anagram of Provider.
Riverpod is a very powerful state management library that allows you to:
- Easily create, access, and combine providers with minimal boilerplate code.
- Write testable code and keep your logic outside the widget tree.
- Catch programming errors at compile-time rather than at runtime.
By design, Provider is an improvement over InheritedWidget
and it depends on the widget tree. This leads to some drawbacks such as:
- Combining Providers is very verbose.
- Getting Providers by type and runtime exceptions.
To combat this, Riverpod is completely independent of the widget tree and doesn't suffer from any of these drawbacks.
4. Redux.
According to the official documentation:
Redux is a predictable state container for Dart and Flutter apps.
Redux uses concepts such as actions, reducers, views, and store, whose flow is shown below:
In summary, actions, like clicks on the UI or events from network operations, are sent to reducers, which turn them into a state. That state is saved in a store, which notifies listeners, like views and components, about changes.
5. BLoC.
According to the official documentation:
The goal of this package is to make it easy to implement the
BLoC
Design Pattern (Business Logic Component).This design pattern helps to separate presentation from business logic. Following the BLoC pattern facilitates testability and reusability. This package abstracts reactive aspects of the pattern allowing developers to focus on writing the business logic.
Think of it as a stream of events: some widgets submit events and other widgets respond to them. BLoC sits in the middle and directs the conversation, leveraging the power of streams.
6. MobX.
According to the official documentation:
MobX is a library for reactively managing the state of your applications. Use the power of observables, actions, and reactions to supercharge your Dart and Flutter applications.
MobX utilizes the following concepts in its functioning:
- Observables: Observables represent the reactive-state of your application. They can be simple scalars to complex object trees.
- Actions: Actions are how you mutate the observables. Rather than mutating them directly, actions add a semantic meaning to the mutations.
- Reactions: They are the observers of the reactive-system and get notified whenever an observable they track is changed.
7. GetX.
According to the official documentation:
GetX is an extra-light and powerful solution for Flutter. It combines high-performance state management, intelligent dependency injection, and route management quickly and practically.
GetX uses two different state managers: the simple state manager (GetBuilder) and the reactive state manager (GetX/Obx).
class SampleController extends GetxController {
@override
void onInit() {
// Here you can fetch you product from server
super.onInit();
}
@override
void onReady() {
super.onReady();
}
@override
void onClose() {
// Here, you can dispose your StreamControllers
// you can cancel timers
super.onClose();
}
}
References.
- https://medium.com/flutter-community/making-sense-all-of-those-flutter-providers-e842e18f45dd
- https://codewithandrea.com/articles/flutter-state-management-riverpod/
- https://blog.novoda.com/introduction-to-redux-in-flutter/
- https://itnext.io/flutter-state-management-with-mobx-and-providers-change-app-theme-dynamically-ba3b60619050
- https://dev.to/gunathilakahashan10/getx-a-superior-state-management-in-flutter-4jcl
While that is a long list of state management options, they are by no means the only options out there, and thus the choice solely depends on your particular use case.
That was indeed all that I have to share for now. To all readers, cheers to code🥂, and have a blessed day.
Posted on January 7, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.