State Management With Provider

See how to architect your Flutter app using Provider, letting you readily handle app state to update your UI when the app state changes. By Jonathan Sande.

4.9 (66) · 3 Reviews

Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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.

Moola X with nonfunctioning favorites UI

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.

Moola X complete app screenshot