This article is a write-up of the highlights in this video 👆, where we compare different state management techniques.
As an example, we use a simple authentication flow. This sets a loading state while a sign-in request is in progress.
For simplicity, this flow is composed of three possible states:
These are represented by the following state machine, which includes a loading state and an authentication state:
When a sign-in request is in progress, we disable the sign-in button and show a progress indicator.
This example app shows how to handle the loading state with various state management techniques.
The main navigation for the sign-in page is implemented with a widget that uses a Drawer menu to choose between different options:
The code for this is as follows:
This widget shows a
AppBar’s title is the name of the selected option
- the drawer uses a custom built
- the body uses a switch to choose between different pages
Reference flow (vanilla)
To enable sign-in, we can start with a simple vanilla implementation that doesn’t have a loading state:
SignInButton is pressed, we call the
This uses Provider to get an
AuthService object, and uses it to sign-in.
AuthServiceis a simple wrapper for Firebase Authentication. See this article for more details.
- The authentication state is handled by an ancestor widget, that uses the
onAuthStateChangedstream to decide which page to show. I covered this in a previous article.
The loading state can be added to the previous implementation by:
- Converting our widget to a
- Declaring a local state variable
- Using it inside our build method
- Updating it before and after the call to sign in.
This is the resulting code:
Top Tip: Note how we use a
finally clause. This can be used to execute some code, whether or not an exception was thrown.
The loading state can be represented by the values of a stream inside a BLoC.
And we need some extra boilerplate code to set things up:
In a nutshell, this code:
- Adds a
StreamController<bool>that is used to handle the loading state
- Makes the
SignInBlocaccessible to our widget with a Provider/Consumer pair inside a
bloc.setIsLoading(value)to update the stream, inside the
- Retrieves the loading state via a
StreamBuilder, and uses it to configure the sign-in button.
Note about RxDart
BehaviourSubject is special stream controller that gives us synchronous access to the last value of the stream.
As an alternative to BloC, we could use a
BehaviourSubject to keep track of the loading state, and update it as needed.
I will update the GitHub project to show how to do this.
ValueNotifier can be used to hold a single value, and notify its listeners when this changes.
This is used to implement the same flow:
ValueNotifier vs ChangeNotifier
ValueNotifier is a subclass of
ChangeNotifier that implements
This is the implementation of
ValueNotifier in the Flutter SDK:
So, when should we use
ValueNotifierif you need widgets to rebuild when a simple value changes.
ChangeNotifierif you want more control on when
Note about ScopedModel
ChangeNotifierProvider is very similar to ScopedModel. In fact these pairs are almost equivalent:
So you don’t need ScopedModel if you are already using Provider, as
ChangeNotifierProvider offers the same functionality.
The three implementations (setState, BLoC, ValueNotifier) are very similar, and only differ in how the loading state is handled.
Here is how they compare:
- setState ↔︎ least amount of code
- BLoC ↔︎ most amount of code
- ValueNotifier ↔︎ middle ground
setState works best for this use case, as we need to handle state that is local to a single widget.
You can evaluate which one is more suitable on a case-by-case basis, as you build your own apps 😉
Bonus: Implementing the Drawer Menu
Keeping track of the currently selected option is also a state management problem:
I first implemented this with a local state variable and
setState, inside the custom drawer menu.
However, the state was lost after sign-in in, because the drawer was removed from the widget tree.
As a solution, I decided to store the state with a
ChangeNotifierProvider<ValueNotifier<Option>> inside the
StreamBuilder controls the authentication state of the user.
And by wrapping this with a
ChangeNotifierProvider<ValueNotifier<Option>>, I’m able to retain the selected option even after the
SignInPageNavigation is removed.
- StatefulWidgets don’t remember their state after they are removed.
- With Provider, we can choose where to store state in the widget tree.
- This way, the state is retained even when the widgets that use it are removed.
ValueNotifier requires a bit more code than
setState. But it can be used to remember the state, by placing a Provider where appropriate in the widget tree.
The example code from this tutorial can be found here:
All these state management techniques are covered in-depth in my Flutter & Firebase Udemy course. This is available for early access at this link (discount code included):