Update note: Mike Katz updated this tutorial for Flutter 3. Jonathan Sande wrote the original.
Through its widget-based declarative UI, Flutter offers a simple way to build views for different states of the app. When the UI needs to reflect a new state, Flutter takes care of rebuilding the necessary components. For example, if a player scores points in a game, the label displaying the current score should update to show the new score.
To manage state changes in Flutter, you need to determine when and where to apply these changes. In an imperative environment, you might use methods like `setText()` or `setEnabled()` to modify a widget’s properties from a callback. In Flutter, however, you let the widgets know that the state has changed so that they can be rebuilt.
The Flutter team recommends using various state management packages and libraries. One of the simplest options is Provider, which allows you to update your UI when the app state changes. In this tutorial, you will learn how to use Provider with ChangeNotifier classes to update views when your model classes change. You will also learn about MultiProvider, which allows you to create a hierarchy of providers within a widget tree, and ProxyProvider, which links two providers together.
Understanding state management is crucial for becoming a proficient Flutter developer. By signing up for a Personal Kodeco Subscription, you can access the Managing State in Flutter video course, which teaches the fundamentals of state management from the ground up.
To get started, you will build a currency exchange app called Moola X. This app allows users to track various currencies and view their values in their preferred currency. Users can also keep track of their currency holdings in a virtual wallet and monitor their net worth. For this tutorial, the currency data will be loaded from a local file instead of a live service to simplify the content and focus on the Provider package. You can download the project materials by clicking the “Download materials” link at the top or bottom of the page.
After building and running the starter app, you will see that it has three tabs: an empty currency list, an empty favorites list, and an empty wallet indicating that the user has no dollars. By default, the base currency for the app is the US Dollar, but you can change it to a different currency by updating the `baseCurrency` variable in `lib/services/currency/exchange.dart`. For example, changing it to `’CAD’` will set the app to Canadian Dollars. After making the change, stop and restart the app to see the updated wallet.
Currently, the app doesn’t have much functionality. In the following sections, you will use Provider to make the app dynamic and update the UI as the user’s actions change the app’s state. The process involves an action triggering a chain of function calls that result in a state change. A Provider listens for these changes and provides the updated values to the widgets that depend on that state. By the end of the tutorial, the app will resemble the provided image.
The first step is to fix the loading of the first tab so that the view updates when the data is loaded. In `lib/main.dart`, the `MyApp` class creates an instance of `Exchange`, which is the service responsible for loading the currency and exchange rate information. When the `build()` method of `MyApp` creates the app widget, it invokes `exchange`’s `load()` method.
In `lib/services/currency/exchange.dart`, you will find the `loadCurrencies()` method, which sets off a chain of futures to load data from the `CurrencyService`. The first future, `loadCurrencies()`, fetches the currencies and updates the internal `currencies` list when the fetch completes.
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` is passed through `CurrencyListViewModel` to separate the view and model logic. The view model class informs the `ListView.builder` on how to construct the table.
Currently, when the app launches, the `Exchange`’s currencies list is empty. As a result, the view model reports that there are no rows to build for the list view. When the data is loaded, the `Exchange`’s data updates, but there is no way to inform the view that the state has changed. Additionally, `CurrencyList` itself is a `StatelessWidget`, so the list will only show the updated data if you select a different tab and then re-select the currencies tab. Manually reloading the view is not an ideal user experience and goes against Flutter’s state-driven declarative UI philosophy.
To automatically update the view, you can use the Provider package. Provider has two main components: a Provider, which manages the lifecycle of the state object and provides it to the dependent view hierarchy, and a Consumer, which builds the widget tree using the value provided by the provider and rebuilds it when the value changes.
In the case of `CurrencyList`, you need to provide the view model object to the list so that it can consume the updates. The view model will listen for changes in the data model (the `Exchange`) and forward those values to the widgets in the view. To use Provider, you need to add it as a dependency in the project. You can do this by running the following command in the terminal from the `moolax` base directory: `flutter pub add provider`. This command adds the latest version of Provider to the `pubspec.yaml` file and downloads the package.
Now that Provider is available, you can use it in the widget. Import the package by adding `import ‘package:provider/provider.dart’;` to the top of `lib/ui/views/currency_list.dart`. Then, replace the existing `build()` method with the updated code provided. This new method demonstrates the main concepts of Provider: the Provider and Consumer. The `ChangeNotifierProvider` widget manages the lifecycle of the provided value, and the inner widget tree that depends on it gets updated when the value changes. The `create` block instantiates the view model object, and the `Consumer` widget uses the provider for `CurrencyListViewModel` and passes the provided value to the builder method. Finally, the builder method returns the same `ListView` created by the helper method as before. As the `CurrencyListViewModel` notifies its listeners of changes, the Consumer provides the new value to its children.
Note: In tutorials and documentation examples, the Consumer is often the immediate child of the Provider, but it can be placed anywhere.