Update note: Mike Katz updated this tutorial for Flutter 3. Jonathan Sande wrote the original.
Through its widget-based declarative UI, Flutter makes a simple promise; describe how to build the views for a given state of the app. If the UI needs to change to reflect a new state, the toolkit will take care of figuring out what needs to be rebuilt and when. For example, if a player scores points in game, a “current score” label’s text should update to reflect the new score state.
The concept called state management covers coding when and where to apply the state changes. When your app has changes to present to the user, you’ll want the relevant widgets to update to reflect that state. In an imperative environment you might use a method like a setText()
or setEnabled()
to change a widget’s properties from a callback. In Flutter, you’ll let the relevant widgets know that state has changed so they can be rebuilt.
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, which you’ll learn how to use here.
In this tutorial you’ll learn:
- How to use
Provider
withChangeNotifier
classes to update views when your model classes change. - Use of
MultiProvider
to create a hierarchy of providers within a widget tree. - Use of
ProxyProvider
to link two providers together.
Getting Started
In this tutorial you’ll build out a currency exchange app, Moola X. This app lets its user keep track of various currencies and see their current value in their preferred currency. The user can also keep track of how much they have of a particular currency in a virtual wallet and track their net worth. In order to simplify the tutorial and keep the content focused on the Provider package, the currency data is loaded from a local data file instead of a live service.
Download the project by clicking the Download materials link at the top or bottom of the page. Build and run the starter app.
You’ll see the app has three tabs: an empty currency list, an empty favorites list, and an empty wallet showing that the user has no dollars. For this app is the base currency, given the author’s bias, is the US Dollar. If you’d like to work with a different base currency, you can update it in lib/services/currency/exchange.dart. Change the definition of baseCurrency
to whatever you’d like, such as CAD
for Canadian Dollars, GBP
for British Pounds, or EUR
for Euros, and so on…
For example, this substitution will set the app to Canadian Dollars:
final String baseCurrency = 'CAD';
Stop and restart the app. The wallet will now show you have no Canadian Dollars. As you build out the app the exchange rates will calculate. :]
Restore the app to “USD or whichever currency you would like to use.
As you can see, the app doesn’t do much yet. Over the next sections you’ll build out the app’s functionality. Using Provider you’ll make it dynamic to keep the UI updated as the user’s actions changes the app’s state changes.
The process is as follows:
- The user, or some other process, takes an action.
- The handler or callback code initiates a chain of function calls that result in a state change.
- A Provider that is listening for those changes provides the updated values to the widgets that listen, or consume that new state value.
Once you’re all done with the tutorial, the app will look something like this:
Providing State Change Notifications
The first thing to fix is the loading of the first tab, so the view updates when the data comes in. In lib/main.dart, MyApp
creates a instance of Exchange
which is the service that loads the currency and exchange rate information. When the build()
method of MyApp
creates the app widget, it invokes exchange’s load()
.
Open lib/services/currency/exchange.dart. You’ll see that load()
sets of a chain of Futures that load data from the CurrencyService
. The first Future
is loadCurrencies()
, shown below:
Future loadCurrencies() {
return service.fetchCurrencies().then((value) {
currencies.addAll(value);
});
}
In the above block, when the fetch completes, the completion block updates the internal currencies
list with the new values. Now, there is a state change.
Next, take a look at lib/ui/views/currency_list.dart. The CurrencyList
widget displays a list of all the known currencies in the first tab. The information from the Exchange
goes through CurrencyListViewModel
to separate the view and model logic. The view model class then informs the ListView.builder
how to construct the table.
When the app launches, the Exchange
‘s currencies
list is empty. Thus the view model reports there aren’t any rows to build out for the list view. When its load completes, the Exchange
‘s data updates but there is no way to inform the view that the state changed. In fact, CurrencyList
itself is a StatelessWidget
.
You can get the list to show the updated data by selecting a different tab, and then re-selecting the currencies tab. When the widget builds the second time, the view model will have the data ready from the exchange to fill out the rows.
Manually reloading the view may be a functional workaround, but it’s hardly a good user experience; it’s not really in the spirit of Flutter’s state-driven declarative UI philosophy. So, how to make this happen automatically?
This is where the Provider package comes in to help. There are two parts to the package that enable widgets to update with state changes:
- A Provider, which is an object that manages the lifecycle of the state object, and “provides” it to the view hierarchy that depends on that state.
- A Consumer, which builds the widget tree that uses the value supplied by the provider, and will be rebuilt when that value changes.
For the CurrencyList
, the view model is the object that you’ll need to provide to the list to consume for updates. The view model will then listen for updates to the data model — the Exchange
, and then forward that on with values for the views’ widgets.
Before you can use Provider
, you need to add it as one of the project’s dependencies. One straightforward way to do that is open the moolax base directory in the terminal and run the following command:
flutter pub add provider
This command adds the latest version Provider
version to the project’s pubspec.yaml file. It also downloads the package and resolves its dependencies all with one command. This saves the extra step of manually looking up the current version, manually updating pubspec.yaml and then calling flutter pub get
.
Now that Provider
is available, you can use it in the widget. Start by adding the following import to the top of lib/ui/views/currency_list.dart at // TODO: add import
:
import 'package:provider/provider.dart';
Next, replace the existing build()
with:
@override
Widget build(BuildContext context) {
// 1
return ChangeNotifierProvider<CurrencyListViewModel>(
// 2
create: (_) => CurrencyListViewModel(
exchange: exchange,
favorites: favorites,
wallet: wallet
),
// 3
child: Consumer<CurrencyListViewModel>(
builder: (context, model, child)
{
// 4
return buildListView(model);
}
),
);
}
This new method exercises the main concepts/classes from Provider
: the Provider and Consumer. It does so with the following four methods:
- A
ChangeNotifierProvider
is a widget that manages the lifecycle of the provided value. The inner widget tree that depends on it gets updated when its value changes. This is the specific implementation ofProvider
that works withChangeNotifier
values. It listens for change notifications to know when to update. - The
create
block instantiates the view model object so the provider can manage it. - The
child
is the rest of the widget tree. Here, aConsumer
uses the provider for theCurrencyListViewModel
and passes its provided value, the created model object, to thebuilder
method. - The
builder
now returns the sameListView
created by the helper method as before.
As the created CurrencyListViewModel
notifies its listeners of changes, the Consumer
provides the new value to its children.
Note: In tutorials and documentation examples, the Consumer
often comes as the immediate child of the Provider
but that is not required. The consumer can be placed anywhere within the child tree.
The code is not ready yet, as CurrencyListViewModel
is not a ChangeNotifier
. Fix that by opening lib/ui/view_models/currency_list_viewmodel.dart.
First, change the class definition by adding ChangeNotifier
as a mixin by replacing the line under // TODO: replace class definition by adding mixin
:
class CurrencyListViewModel with ChangeNotifier {
Next, add the following body to the constructor CurrencyListViewModel()
by replacing the // TODO: add constructor body
with:
{
exchange.addListener(() {notifyListeners();}); // <-- temporary
}
Now the class is a ChangeNotifier
. It is provided by the ChangeNotifierProvider
in CurrencyList
. It’ll also listen to changes in the exchange and forward them as well. This last step is just a temporary workaround to get the table to load right away. You’ll clean this up later on when you learn to work with multiple providers.
The final piece to fix the compiler errors is adding ChangeNotifier
to Exchange
. Again, open lib/services/currency/exchange.dart.
At the top of the file, add this import at the // TODO: add import
:
import 'package:flutter/foundation.dart';
ChangeNotifier
is part of the Foundation
package, so this makes it available to use.
Next, add it as a mixin by changing the class definition at the // TODO: update class definition/code> to:
class Exchange with ChangeNotifier {
Like with CurrencyListViewModel
, this enables the Exchange
to allow other objects to listen for change notifications. To send the notifications, update the completion block of loadExchangeRates()
by replacing the method with:
Future loadExchangeRates() {
return service.fetchRates().then((value) {
rates = value;
notifyListeners();
});
}
This adds a call to notifyListeners
when fetchRates
completes at the end of the chain of events kicked by load()
.
Build and run the app again. This time, once the load completes, the Exchange
will notify the CurrencyListViewModel
and it’ll then notify the Consumer
in CurrencyList
which will then update its children and the table will be redrawn.