Flutter State Management:

Ojas Gupta
DataX Journal
Published in
15 min readFeb 11, 2021

--

State Management With Provider

The Flutter team recommends that beginners to Flutter development use Provider for state management. But what is Provider exactly, and how do you use it?

Provider is a UI tool. If you’re confused about the differences between architecture, state management and UI tools, you’re not alone. This tutorial will help you understand the concepts and give you a blueprint for building real apps. It might even help you become a millionaire in the process!

In this tutorial, you’ll learn the ins and outs of managing states with Provider by creating a currency exchange app called MoolaX. While building this app, you’ll elevate your Flutter development skills from beginner to intermediate level by learning how to:

  • Architect your app.
  • Implement a Provider.
  • Manage app state like a pro.
  • Update the UI based on app state changes.

Note: This tutorial assumes you are comfortable with Dart and know how to build a simple Flutter app by composing widgets into a tree. If you are new to Flutter, check out Getting Started with Flutter first.

Getting Started

Download the project by clicking the Download Materials button at the top or bottom of the page. You’ll learn more from this tutorial by following along step-by-step than you will if you only read through it.

This tutorial uses Android Studio, but Visual Studio Code will work fine as well.

Moola X will let you select and convert between different currencies. Here’s what it will look like when you’re done:

Open the starter project by navigating to the starter folder and clicking Get dependencies when Android Studio prompts you to do so.

The starter project already includes some code so you can finish this project in a single tutorial. However, the tutorial will walk you through how to create each part so that you can follow the same pattern when you make your own apps.

Run the app now and you’ll see this:

It doesn’t do much yet.

But before you add some code, first some theory on the purpose of Provider. :]

Architecting Your App

If you haven’t heard about the principles of clean architecture, please read about them before continuing.

The goal is to keep core business logic separate from the UI, database, network and third-party packages. Why? The core business logic doesn’t change frequently, while all the others often do.

The UI shouldn’t communicate directly with the web. You shouldn’t scatter direct calls to the database across your app. Everything goes through the business logic from a single location.

What does this mean? It gives you a plug-in architecture. You can swap one database framework for another and the rest of the app doesn’t even know there was a change. You can replace your mobile UI with a desktop UI and the rest of the app doesn’t care. This is useful for making scalable, maintainable and testable apps.

State Management and Provider

The Moola X app architecture follows this principle. The business logic in the middle handles the calculations related to currency exchange. Local storage, the web API, and the UI along with Flutter and Provider, are all completely separate from the business logic and from one another.

The local storage uses shared preferences, but that’s an implementation detail that doesn’t affect the rest of the app. Likewise, where the web API gets its data doesn’t matter to the rest of the app.

This next part is important to understand: The UI, Flutter, and Provider are all contained in one part of the app. Flutter itself is a UI framework and Provider is a widget for that framework.

Is Provider the architecture? No.

Is Provider the state management? No, not in this app anyway.

The state is the current value of the app’s variables. These variables are part of the business logic, grouped and managed by model objects. Thus, the business logic manages the state, not Provider.

So, what is Provider?

It’s a state management helper. It’s a widget that makes some value — like a state model object — available to the widgets below it.

A Consumer widget, which is also part of the Provider package, listens for changes in the value and then rebuilds the widgets below itself when changes occur.

See the Manage State with Provider video series for a fuller explanation of state and Providers. There are many different kinds of Providers, most of which are outside the scope of this tutorial.

Communicating With the Business Logic

This tutorial follows an architectural pattern inspired by FilledStacks. It separates the architecture cleanly without being overly complicated.

Furthermore, this tutorial simplifies the setup to make it more accessible to beginners.

For communication between the UI and the business logic, the architecture is similar to MVVM (Model View ViewModel).

The model is the data from a source like a database or the web. The view is the UI, like a screen or widget. The view model is the business logic sitting between the UI and the data. It provides data in a form that the UI can present, but it knows nothing about the UI itself. This differs from the MVP architecture. The view model also doesn’t know where the data comes from. That’s abstracted away.

In MoolaX, each screen will have its own view model. The data will come from both the web and local storage. Classes to handle that are called services because they do some work for the business logic.

Here is a diagram that closely resembles the architecture of Moola X.

Note these points:

  • The UI screens listen for changes in the view models. They also send events to the view models.
  • The view models don’t know any details about how the UI looks.
  • The business logic interacts with an abstract currency service. It doesn’t know anything about the local storage or the web. This type of architecture is sometimes called a data repository.

Enough theory. It’s time to make this app!

Creating the Core Business Logic

Take a look at the project folder structure in Android Studio (be sure to choose the Project view in the Project pane and not the Android view):

Note that the main folders, business_logic, services and ui closely mirror the architectural diagram that you saw above.

Models

Now, take a look at the models folder:

These are the data structures that the business logic uses, which you can also call entities. This tutorial uses them to pass data from the services, too, though some people recommend using separate models for that.

The Class Responsibility Collaboration (CRC) card model is a good way to decide which data models your app needs. Here are the cards for this app (click the image for a larger view):

In the end, the app only uses the Currency and Rate data structures. These represent the cash and the exchange rate that you’d need even if you didn’t have a computer.

View Models

The job of a view model is to take the data and put it in a presentable form that the UI can use.

Expand view_models. You’ll see that there are two view models, one for the screen that calculates the actual currency exchange and another for choosing which currencies you want to convert.

Since the process for making any view model is basically the same, you’ll only make the view model for choosing your favorite currencies. The starter project already includes the other one.

Open choose_favorites_viewmodel.dart. Ignore the gray squiggle lines beneath some of the import code at the top of the file. These squiggles mean the libraries are not currently in use, but you’ll use them soon.

Beneath the top-most import statements, you’ll see the following code.

// 1
import 'package:flutter/foundation.dart';
// 2
class ChooseFavoritesViewModel extends ChangeNotifier {
// 3
final CurrencyService _currencyService = serviceLocator<CurrencyService>();
List<FavoritePresentation> _choices = [];
List<Currency> _favorites = [];
// 4
List<FavoritePresentation> get choices => _choices;
void loadData() async {
// ...
// 5
notifyListeners();
}
void toggleFavoriteStatus(int choiceIndex) {
// ...
// 5
notifyListeners();
}
}

Note the following things in the code above:

  1. To let the UI listen for changes in the view model, you use ChangeNotifier. This class is part of the Flutter foundation package.
  2. You’re extending the view model class with ChangeNotifier. Another option would be to use a mixin. ChangeNotifier provides notifyListeners(), which you’ll use below.
  3. A service handles the work of getting and saving the currencies and exchange rates. Here, you get an implementation of the abstract CurrencyService. The actual implementation is hidden from the view model. You can swap it out with different implementations, or even fake data, and the view model will be none the wiser.
  4. Anyone who has a reference to this view model can access a list of currencies choices that the user can favorite. The UI will use that list to create a clickable ListView.
  5. After loading the list of currencies or changing the favorite status of a currency, you can notify the listeners. The UI will listen so it can reflect the change visually.

Still in choose_favorites_viewmodel.dart, look below ChooseFavoritesViewModel. There’s a second class called FavoritePresentation:

class FavoritePresentation {
final String flag;
final String alphabeticCode;
final String longName;
bool isFavorite;
FavoritePresentation(
{this.flag, this.alphabeticCode, this.longName, this.isFavorite,});
}

To keep the data in a simple format, the member variables of this model class are all strings. Remember to keep as much logic out of the UI as possible.

In ChooseFavoritesViewModel, replace the loadData() method with the following (including adding two private helper methods):

void loadData() async {
final rates = await _currencyService.getAllExchangeRates();
_favorites = await _currencyService.getFavoriteCurrencies();
_prepareChoicePresentation(rates);
notifyListeners();
}
void _prepareChoicePresentation(List<Rate> rates) {
List<FavoritePresentation> list = [];
for (Rate rate in rates) {
String code = rate.quoteCurrency;
bool isFavorite = _getFavoriteStatus(code);
list.add(FavoritePresentation(
flag: IsoData.flagOf(code),
alphabeticCode: code,
longName: IsoData.longNameOf(code),
isFavorite: isFavorite,
));
}
_choices = list;
}
bool _getFavoriteStatus(String code) {
for (Currency currency in _favorites) {
if (code == currency.isoCode)
return true;
}
return false;
}

Here, loadData() asks the currency service for a list of all the available exchange rates and also the ones that the user has favorited. Next, _prepareChoicePresentation() converts the list to a form that the UI can easily present. The helper _getFavoriteStatus() just determines whether a currency is favorited.

To finish up the view model logic, replace the toggleFavoriteStatus() method with the following (again, adding two helper methods that in this case add and remove currency choices from favorites):

void toggleFavoriteStatus(int choiceIndex) {
final isFavorite = !_choices[choiceIndex].isFavorite;
final code = _choices[choiceIndex].alphabeticCode;
_choices[choiceIndex].isFavorite = isFavorite;
if (isFavorite) {
_addToFavorites(code);
} else {
_removeFromFavorites(code);
}
notifyListeners();
}
void _addToFavorites(String alphabeticCode) {
_favorites.add(Currency(alphabeticCode));
_currencyService.saveFavoriteCurrencies(_favorites);
}
void _removeFromFavorites(String alphabeticCode) {
for (final currency in _favorites) {
if (currency.isoCode == alphabeticCode) {
_favorites.remove(currency);
break;
}
}
_currencyService.saveFavoriteCurrencies(_favorites);
}

As soon as toggleFavoriteStatus() is called, the view model asks the currency service to save the new favorite status. Since it also calls notifyListeners(), the UI immediately shows the change in state as well.

Congratulations, you’ve now finished the view model.

In summary, all your view model needs to do is to extend ChangeNotifier and call notifyListeners() whenever it needs to update the UI with the new state. You can follow this pattern even with futures and streams.

Services

A service is a class that does some work for you. You have three services in your app: Currency Exchange, Storage, and Web API. Look at the app architectural diagram and you’ll see the three services on the right in red.

The process for making a service in Dart is pretty easy.

  1. First you make an abstract class containing the methods you’ll need.
  2. Then you write a concrete implementation of the abstract class.

Since the process of making a service is the same every time, you’ll only make the Web API Service. The starter project already contains the Currency Exchange Service and Storage Service.

Creating an Abstract Service Class

To start making the Web API Service, open web_api.dart.

You’ll see the following code:

import 'package:moolax/business_logic/models/rate.dart';abstract class WebApi {
Future<List<Rate>> fetchExchangeRates();
}

The class is abstract, so it doesn’t actually do anything. However, it does tell you what your app expects it to do: It should get a list of exchange rates from the web. How you actually do that is up to you.

Having the freedom to easily change implementations means that, during development, you can use fake data. Just hard-code it so the app thinks it’s from the web. Then come back later and write the code to access the web.

Using Fake Data

In web_api, create a new file called web_api_fake.dart. Then paste in the the following:

import 'package:moolax/business_logic/models/rate.dart';
import 'web_api.dart';
class FakeWebApi implements WebApi { @override
Future<List<Rate>> fetchExchangeRates() async {
List<Rate> list = [];
list.add(Rate(
baseCurrency: 'USD',
quoteCurrency: 'EUR',
exchangeRate: 0.91,
));
list.add(Rate(
baseCurrency: 'USD',
quoteCurrency: 'CNY',
exchangeRate: 7.05,
));
list.add(Rate(
baseCurrency: 'USD',
quoteCurrency: 'MNT',
exchangeRate: 2668.37,
));
return list;
}
}

This class implements the abstract WebApi class, but it returns some hardcoded data. Now, you can happily go on coding the rest of your app without worrying about internet connection problems or long wait times. Whenever you’re ready, come back and write the actual implementation that queries the web.

Adding a Service Locator

Even though you’ve finished creating a fake implementation of WebApi, you still need to tell the app to use that implementation.

You’ll do that using a service locator. A service locator is an alternative to dependency injection. The point of both of these architectural techniques is to decouple a class or service from the rest of the app.

Think back to ChooseFavoritesViewModel; there was a line like this:

final CurrencyService _currencyService = serviceLocator<CurrencyService>();

The serviceLocator is a singleton object that knows all the services your app uses.

In services, open service_locator.dart. You’ll see the following:

// 1
GetIt serviceLocator = GetIt.instance;
// 2
void setupServiceLocator() {
// 3
serviceLocator.registerLazySingleton<StorageService>(() => StorageServiceImpl());
serviceLocator.registerLazySingleton<CurrencyService>(() => CurrencyServiceFake());
// 4
serviceLocator.registerFactory<CalculateScreenViewModel>(() => CalculateScreenViewModel());
serviceLocator.registerFactory<ChooseFavoritesViewModel>(() => ChooseFavoritesViewModel());
}

Here’s what this code does:

  1. GetIt is a service locator package named get_it that’s predefined in pubspec.yaml under dependencies. Behind the scenes, get_it keeps track of all your registered objects. The service locator is a global singleton that you can access from anywhere within your app.
  2. This function is where you register your services. You should call it before you build the UI. That means calling it first thing in main.dart.
  3. You can register your services as lazy singletons. Registering it as a singleton means that you’ll always get the same instance back. Registering it as a lazy singleton means that the service won’t be instantiated until you need it the first time.
  4. You can also use the service locator to register the view models. This makes it convenient for the UI to get a reference to them. Note that instead of a singleton, they’re registered as a factory. That means that every time you request a view model from the service locator, it gives you a new instance of the view model.

Just to see where the code calls setupServiceLocator(), open main.dart.

void main() {
setupServiceLocator(); // <--- here
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Moola X',
theme: ThemeData(
primarySwatch: Colors.indigo,
),
home: CalculateCurrencyScreen(),
);
}
}

There it is, right before you call runApp(). That means that your entire app will have access to the service locator.

Registering FakeWebApi

You still haven’t registered FakeWebApi, so go back to service_locator.dart. Register it by adding the following line to the top of setupServiceLocator:

serviceLocator.registerLazySingleton<WebApi>(() => FakeWebApi());

Also, replace CurrencyServiceFake with CurrencyServiceImpl.

serviceLocator.registerLazySingleton<CurrencyService>(() => CurrencyServiceImpl());

The starter project was temporarily using CurrencyServiceFake so that the app wouldn't crash when you ran it before this point.

Import missing classes by adding the following code just below the other import statements:

import 'web_api/web_api.dart';
import 'web_api/web_api_fake.dart';
import 'currency/currency_service_implementation.dart';

Build and run the app and press the Heart Action button on the toolbar.

At this point, you still can’t see the favorites because you haven’t finished the UI.

Concrete Web API Implementation

Since you’ve already registered the fake web API service, you could go on and finish the rest of the app. However, to keep all the service-related work in one section of this tutorial, your next step is to implement the code to get the exchange rate data from a real server.

In services/web_api, create a new file called web_api_implementation.dart. Add in the following code:

import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:moolax/business_logic/models/rate.dart';
import 'web_api.dart';
// 1
class WebApiImpl implements WebApi {
final _host = 'api.exchangeratesapi.io';
final _path = 'latest';
final Map<String, String> _headers = {'Accept': 'application/json'};
// 2
List<Rate> _rateCache;
Future<List<Rate>> fetchExchangeRates() async {
if (_rateCache == null) {
print('getting rates from the web');
final uri = Uri.https(_host, _path);
final results = await http.get(uri, headers: _headers);
final jsonObject = json.decode(results.body);
_rateCache = _createRateListFromRawMap(jsonObject);
} else {
print('getting rates from cache');
}
return _rateCache;
}
List<Rate> _createRateListFromRawMap(Map jsonObject) {
final Map rates = jsonObject['rates'];
final String base = jsonObject['base'];
List<Rate> list = [];
list.add(Rate(baseCurrency: base, quoteCurrency: base, exchangeRate: 1.0));
for (var rate in rates.entries) {
list.add(Rate(baseCurrency: base,
quoteCurrency: rate.key,
exchangeRate: rate.value as double));
}
return list;
}
}

Note the following points:

  1. Like FakeWebApi, this class also implements the abstract WebApi. It contains the logic to get the exchange rate data from api.exchangeratesapi.io. However, no other class in the app knows that, so if you wanted to swap in a different web API, this is the only place where you'd need to make the change.
  2. The site exchangeratesapi.io graciously provides current exchange rates for a select number of currencies free of charge and without requiring an API key. To be a good steward of this service, you should cache the results to make as few requests as possible. A better implementation might even cache the results in local storage with a time stamp.

Open service_locator.dart again. Change FakeWebApi() to WebApiImpl() and update the import statement for the switch to WebApiImpl():

import 'web_api/web_api_implementation.dart';void setupServiceLocator() {
serviceLocator.registerLazySingleton<WebApi>(() => WebApiImpl());
// ...
}

Implementing Provider

Now it’s time for Provider. Finally! This is supposed to be a Provider tutorial, right?

This is a tutorial about architecture, state management and Provider. By waiting this long to get to Provider, you should now realize that Provider is a very small part of your app. It’s a convenient tool for passing state down the widget tree and rebuilding the UI when there are changes, but it isn’t anything like a full architectural pattern or state management system.

Find the Provider package in pubspec.yaml:

dependencies:
provider: ^4.0.1

There’s a special Provider widget called ChangeNotifierProvider. It listens for changes in your view model class that extends ChangeNotifier.

In ui/views, open choose_favorites.dart. Replace this file with the following code:

import 'package:flutter/material.dart';
import 'package:moolax/business_logic/view_models/choose_favorites_viewmodel.dart';
import 'package:moolax/services/service_locator.dart';
import 'package:provider/provider.dart';
class ChooseFavoriteCurrencyScreen extends StatefulWidget {
@override
_ChooseFavoriteCurrencyScreenState createState() =>
_ChooseFavoriteCurrencyScreenState();
}
class _ChooseFavoriteCurrencyScreenState
extends State<ChooseFavoriteCurrencyScreen> {
// 1
ChooseFavoritesViewModel model = serviceLocator<ChooseFavoritesViewModel>();
// 2
@override
void initState() {
model.loadData();
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Choose Currencies'),
),
body: buildListView(model),
);
}
// Add buildListView() here.
}

You’ll add the buildListView() method in just a minute. First note the following things:

  1. The service locator returns a new instance of the view model for this screen.
  2. You’re using StatefulWidget because it gives you the initState() method. This allows you to tell the view model to load the currency data.

Just below build(), add the following buildListView() implementation:

Widget buildListView(ChooseFavoritesViewModel viewModel) {
// 1
return ChangeNotifierProvider<ChooseFavoritesViewModel>(
// 2
create: (context) => viewModel,
// 3
child: Consumer<ChooseFavoritesViewModel>(
builder: (context, model, child) => ListView.builder(
itemCount: model.choices.length,
itemBuilder: (context, index) {
return Card(
child: ListTile(
leading: SizedBox(
width: 60,
child: Text(
'${model.choices[index].flag}',
style: TextStyle(fontSize: 30),
),
),
// 4
title: Text('${model.choices[index].alphabeticCode}'),
subtitle: Text('${model.choices[index].longName}'),
trailing: (model.choices[index].isFavorite)
? Icon(Icons.favorite, color: Colors.red)
: Icon(Icons.favorite_border),
onTap: () {
// 5
model.toggleFavoriteStatus(index);
},
),
);
},
),
),
);
}

Here’s what this code does:

  1. You add ChangeNotifierProvider, a special type of Provider which listens for changes in your view model.
  2. ChangeNotifierProvider has a create method that provides a value to the widget tree under it. In this case, since you already have a reference to the view model, you can use that.
  3. Consumer rebuilds the widget tree below it when there are changes, caused by the view model calling notifyListeners(). The Consumer's builder closure exposes model to its descendants. This is the view model that it got from ChangeNotifierProvider.
  4. Using the data in model, you can build the UI. Notice that the UI has very little logic. The view model preformats everything.
  5. Since you have a reference to the view model, you can call methods on it directly. toggleFavoriteStatus() calls notifyListeners(), of which Consumer is one, so Consumer will trigger another rebuild, thus updating the UI.

Build and run the app now to try it.

So, this is how you should approach state management in flutter. It should be noted that the methods for achieving state management can be different ( Provider, bloc) but the approach is the same!

Happy Learning :)

--

--