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.

Leave a rating/review
Download materials
Save for later
Share
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 with ChangeNotifier 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.
Understanding state management is critical to becoming a competent Flutter developer. By signing up to a Personal Kodeco Subscription, you will gain access to Managing State in Flutter. This video course will teach you the fundamentals of state management from the ground up.

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.

Initial app launch shows blank list

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. :]

App configured for Canadian currency

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:

  1. The user, or some other process, takes an action.
  2. The handler or callback code initiates a chain of function calls that result in a state change.
  3. A Provider that is listening for those changes provides the updated values to the widgets that listen, or consume that new state value.

Update flow for state change

Once you’re all done with the tutorial, the app will look something like this:

First tab with currencies correctly loaded

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.

First tab with currencies correctly loaded

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:

  1. 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 of Provider that works with ChangeNotifier values. It listens for change notifications to know when to update.
  2. The create block instantiates the view model object so the provider can manage it.
  3. The child is the rest of the widget tree. Here, a Consumer uses the provider for the CurrencyListViewModel and passes its provided value, the created model object, to the builder method.
  4. The builder now returns the same ListView 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.

List loading after exchange notifies provider