State Management With Provider
The Flutter team recommends several state management packages and libraries. Provider is one of the simplest to update your UI when the app state changes. By Michael Katz.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
State Management With Provider
30 mins
Keeping a List of Favorites
The app now shows the currency list, you can tap one of the rows to interact it. You can select it as a favorite or update the amount in your wallet.
The buttons are already wired up as part of the starter project, but the UI doesn't yet update to reflect the new state. When the user taps a button, they would expect the screen to show the updated state immediately. You can force an update by leaving a screen and then going back. But wouldn't it be nice though if the widgets updated right away?
Reusing the Provider Pattern
The CurrencyDetail
screen also has a view model that holds its UI state. You can apply Provider here again in the same pattern to have the views update.
Open lib/ui/views/currency_detail.dart. At the top of the file, add the import at the // TODO: add import
:
import 'package:provider/provider.dart';
This makes the package available for use.
Next, replace buildBody()
with:
Widget buildBody(BuildContext context) {
return ChangeNotifierProvider<CurrencyDetailViewModel>(
create: (_) => CurrencyDetailViewModel(
currency: currency,
exchange: exchange,
favorites: favorites,
wallet: wallet,
),
child: Consumer<CurrencyDetailViewModel>(
builder: (context, viewModel, _) =>
assembleElements(context, viewModel),
),
);
}
This is exactly the same procedure you applied to CurrencyList
. It block wraps the CurrencyDetailViewModel
creation in a ChangeNotifierProvider
, and then provides it to the rest of the widget tree through a Consumer
.
Now you have to update CurrencyDetailViewModel
to be a ChangeNotifier
and have it send notifications when there are updates. To do that, open lib\ui\view_models\currency_detail_viewmodel.dart.
Update the class definition by replacing the line after // TODO: update class definition
with:
class CurrencyDetailViewModel with ChangeNotifier {
Then, at the end of both toggleFavorites()
and commitToWallet()
, find the // TODO: add notifyListeners
and replace it with:
notifyListeners();
This tells any listeners, such as the provider, that there are state changes.
Build and run the app again and verify that tapping the buttons update the views right away. Red hearts are for favorite currencies, and green hearts are for favorite currencies that are also in the wallet.
Choosing a Stateful Widget
At this point it might feel like you've introduced a lot of machinery just to refresh a list and change an icon color. Flutter has StatefulWidget
that allows you to create a custom widget that manages its own state. You make one with a custom state object and update it through the setState
method.
In Moola X, if CurrencyDetail
were to use StatefulWidget
instead of Provider, it could track favorite status as an internal Boolean value and wallet amount as a float.
Conceptually using StatefulWidget
makes sense when state changes just affects the widget and its children, like visual changes or navigation information. In Moola X, the state changes are not only reflected on the current screen, but update app-level data models. Therefore, the changes are expected to persist across many screens. Since the state management is broader than one widget, using Provider allows for a more flexible architecture.
Providing Favorites Across Multiple Screens
Since favorites are expected to persist across the app, users expect to see that state is reflected on the currency list when they tap the back button. To make that work, you could go down the same path as CurrencyListViewModel
: each view model can listen for changes and explicitly forward those notifications to their own consumers. But, this pattern is brittle and cumbersome.
It's more straightforward to create a Provider
for the Favorites
model itself at the top of the app and make it available everywhere it's needed. Go up to the top by opening lib/main.dart.
At the top of the file, replace // TODO: add import here
with:
import 'package:provider/provider.dart';
This makes the Provider package available.
Next, replace buildBody()
with:
Widget buildBody() {
final exchange = Exchange(service: CurrencyServiceLocal())..load();
final storage = StorageServiceLocal();
return ChangeNotifierProvider<Favorites>(
create: (_) => Favorites(storage: storage),
child: Consumer<Favorites>(
builder: (_, favorites, __) => buildTabBar(
exchange,
favorites,
Wallet(exchange: exchange, storage: storage)
),
),
);
}
This repeats the ChangeNotifierProvider
/Consumer
same pattern as before. Here, Favorites
is the object provided. The buildTabBar
method reuses the Consumer
's favorites
when constructing both CurrencyList
and FavoriteTable
so it's the same object and will update both tabs when it's notified.
Speaking of updating listeners, you need to fix Favorites
to get the project to compile. Open lib/services/user/favorites.dart. At the top of the file replace // TODO: add import here
with this import:
import 'package:flutter/foundation.dart';
Then, change the class definition with this mixin under // TODO: change class definition
:
class Favorites with ChangeNotifier {
Next, add a call to notifyListeners()
to the completion block in the constructor, by replacing the entire Favorites()
with:
Favorites({required this.storage}) {
storage.getFavoriteCurrencies().then((value) {
_favorites.addAll(value);
notifyListeners();
});
}
Finally, at the end of both toggleFavorite()
and reorder()
, find // TODO: add notifyListeners
and replace it with:
notifyListeners();
These calls make sure that when the state of the favorites updates through loading, setting or changing the list order, any of the listening providers will update the consumers. Thus the UI will update.
Re-run the app and now the currency list's hearts will stay in sync with changes on the detail screen. :]
As a bonus, the table on the favorites tab also now stays in sync with changes to the favorites model. Updates to the favorites state or use of the re-ordering controls now work. This happened because the FavoritesTable
is also a child of the Consumer
of the favorites Provider
.
Keeping Track of Two Models
If you try to make a currency a favorite and add an amount of it to the wallet, you'll see the heart turn green. That's green for cash, apologies to those who live places with colorful money.
Unfortunately, as you navigate through the app, the heart may only be red, or may switch between red and green, depending on the state of the Wallet
at the time that the widget builds.
The solution for this is to also provide the wallet's updates along with the favorites to the various view models to compute the correct color for the heart icons. You can do this by nesting a new ChangeNotifierProvider
for the wallet as the child
of the favorites provider. Nesting providers makes code that is hard to read.
Fortunately, the Provider package provides a syntactically nice helper in the form of MultiProvider
. It lets you create multiple providers at once in an array.
Open lib/main.dart.
Replace buildBody()
with:
Widget buildBody() { final exchange = Exchange(service: CurrencyServiceLocal())..load(); final storage = StorageServiceLocal(); // 1 return MultiProvider( // 2 providers: [ // 3 ChangeNotifierProvider<Favorites>(create: (_) => Favorites(storage: storage) ), // 4 ChangeNotifierProvider<Wallet>(create: (_) => Wallet(exchange: exchange, storage: storage) ), ], // 5 child: Consumer2<Favorites, Wallet>( // 6 builder: (_, favorites, wallet, __) => buildTabBar(exchange, favorites, wallet), ), ); }
This updated method replaces what you did before to make use of MultiProvider
. Take note of the following lines of code:
-
MultiProvider
is aProvider
widget that simplifies a hierarchy of nested providers into a list. - The
providers
list is an arbitrary list of providers, here there are two of them. - The
Favorites
provider is the same as it was before. - Now the
Wallet
creation is wrapped in a newChangeNotifierProvider
.Wallet
will be updated below to be aChangeNotifier
. -
Consumer
widgets can be used anywhere down the tree and can be nested as well. To keep the code simpler, there are handyConsumerN
widgets for consuming multiple providers in a single block. Here both theWallet
andFavorites
providers can be used in the same builder. Note that the order of the types will match the parameters of thebuilder
. - The provided values are available in the
builder
function along with the build context just like the single consumer.
Now that the main view is wired up, you need to add ChangeNotifier
to Wallet
for the app to compile.
Open lib/services/user/wallet.dart and replace // TODO: add import
with:
import 'package:flutter/foundation.dart';
Then update the class definition, by replacing the line under // TODO: update class definition
with:
class Wallet with ChangeNotifier {
And finally, replace all the //TODO: add notifyListeners
lines with:
notifyListeners();
You should have found three places to update:
- In the constructor, at the end of the storage
loadWallet()
completion block. - At the end of the constructor.
- At the end of
save()
.
Rebuild and run the app. The favorites and wallet models will update across the app. Changes in the detail page will reflect on all three tabs. You'll now see the properly colored hearts on the currency list and dollar totals on the wallet tab.