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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
State Management With Provider
30 mins
- Getting Started
- Architecting Your App
- State Management and Provider
- Communicating With the Business Logic
- Creating the Core Business Logic
- Models
- View Models
- Services
- Creating an Abstract Service Class
- Using Fake Data
- Adding a Service Locator
- Registering FakeWebApi
- Concrete Web API Implementation
- Implementing Provider
- Becoming a Millionaire
- Using Provider in Large-Scale Apps
- Other Architectural and State Management Options
- Where to Go From Here?
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:
-
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. - 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.
- 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.
- 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:
- Like
FakeWebApi
, this class also implements the abstractWebApi
. 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. - 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:
- The service locator returns a new instance of the view model for this screen.
- You're using
StatefulWidget
because it gives you theinitState()
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:
- You add
ChangeNotifierProvider
, a special type of Provider which listens for changes in your view model. -
ChangeNotifierProvider
has acreate
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. -
Consumer
rebuilds the widget tree below it when there are changes, caused by the view model callingnotifyListeners()
. The Consumer'sbuilder
closure exposesmodel
to its descendants. This is the view model that it got fromChangeNotifierProvider
. - Using the data in
model
, you can build the UI. Notice that the UI has very little logic. The view model preformats everything. - Since you have a reference to the view model, you can call methods on it directly.
toggleFavoriteStatus()
callsnotifyListeners()
, of whichConsumer
is one, soConsumer
will trigger another rebuild, thus updating the UI.
Build and run the app now to try it.