What is good about this article is that it not only explains how the Flutter Provider manages State, but also what architecture a Flutter App can adopt. This architecture is based on the architectural principles of Clean Architecture and FilledStacks. However, the model of MVVM is adopted in the end.

More importantly, the Provider described in this article is a widget. Use this widget with the Consumer widget to make the UI = f(state) change as the state changes.

Finally, or the sentence to see the original please here, the article itself has quality, and write is not difficult.

The body of the

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

Provider is a UI tool. If you’re confused about architecture, state, and architecture, you’re not alone. This article will help you clarify these concepts and show you how to write an app from scratch.

This article will walk you through all aspects of Provider management of state. So let’s write an app that calculates the exchange rate, which is called MoolaX. You will improve your Flutter skills while writing this app:

  1. App architecture
  2. Implementing a Provider
  3. Skillfully manage app state
  4. Update the UI based on changes in state

Note: This article assumes that you already know Dart and how to write a Flutter app. If you are not clear on this, please move to the introduction of Flutter.

start

Click Download Materials to download the code for the project. You can then follow this article step by step to add code to complete the development.

This article uses Android Studio, but Visual Studio Code is also available. VS Code is actually better.

You can choose different currencies in MoolaX. The App runs like this:

Open the original project and unzip the starter directory. Android Studio will pop up a box, click Get Dependencies.

Some of the code was already included in the initial project, but this tutorial will walk you through adding the necessary code so you can easily learn what follows.

The app now runs like this:

Build the App architecture

If you haven’t heard of Clean Architecture, please read this article before continuing.

The idea is to separate the core business logic from the UI, database, network requests, and third-party packages. Why is that? The core business logic changes relatively infrequently.

The UI should not directly request the network. You shouldn’t write code all over the place that reads and writes to the database. The business logic is that all data should come from one unified place.

This creates a plug-in system. Even if you change a database, the rest of the app doesn’t notice. You can switch from a mobile UI to a desktop UI, and the rest of the app doesn’t matter. This is very useful for developing an app that is easy to maintain and extend.

Use the Provider to manage state

MoolaX’s architecture fits this principle. The business logic handles calculations related to exchange rates. Local Storage, network requests, and the UI and Provider of Flutter are all independent of each other.

The Local storage uses shared Preferences, but this is not associated with the rest of the app. Similarly, how web requests get data has nothing to do with the rest of the app.

The next thing to understand is that THE UI, Flutter, and Provider are all in the same section. A Provider is a widget inside a Flutter UI framework.

Is a Provider an architecture? It isn’t. Is Provider state management? No, at least not in this app.

State is the current value of the app variable. These variables are part of the app’s business logic, scattered and managed in different Model objects. So, the business logic manages state, not the Provider.

So what exactly is a Provider?

It’s a helper for state management, and it’s a widget. This widget allows you to pass a Model object to its child widgets.

The Consumer Widget, which is part of the Provider package, listens for changes in the mode value exposed by the Provider and rebuilds all of its child widgets.

The Managing State with Provider series provides a more comprehensive analysis of state and Provider. There are many kinds of providers, but most are outside the scope of this article.

Communicate with business logic

The architectural pattern for text was inspired by FilledStacks. It allows the architecture to be organized without being too complex. It’s also friendly to beginners.

This Model is very similar to MVVM (Model View ViewModel).

A model is data requested from a database or network. A View is a UI. It can also be a screen or widget. The ViewModel is the business logic between the UI and the data, and provides the data that the UI can present. But it’s not aware of the UI. It’s different than MVP. The ViewModel should also not know where the data comes from.

In MoolaX, each page has its own view Model. Data can be obtained from network and local storage. The class that handles this is called Services. MoolaX’s architecture looks like this:

Note the following points:

  • The UI page listens for changes to the View Model and also sends events to the View Model
  • The View Model is not aware of the details of the UI
  • Business logic interacts with currency abstraction. It does not sense whether the data came from a network request or from local storage.

That’s it for the theory part, now for the code part!

Create core business logic

The directory structure of the project is as follows:

Models

Let’s take a look at the mdels directory:

These are the data structures that the business logic uses. The class responsibility collaborative card model is a good way to determine which models are needed. The cards are as follows:

Finally, Currency and Rate models will be used. They represent advances and exchange rates, which computers need even if you don’t have them.

View Model

The view Mode’s job is to take the data and convert it to a usable format for the UI.

Expand the view_models directory. You’ll see two View Models, one for the settlement page and one for the exchange rate selection page.

Open the choose_favorites_viewmodel. Dart. You should 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) {
    // ...
    / / 5notifyListeners(); }}Copy the code

Explanation:

  1. useChangeNotifierTo implement the UI to view Model listening. This class is FlutterfoundationThe package.
  2. The View Model class inheritsChangeNotifierClass. Another option is to use mixins.ChangeNotifierThere is anotifyListeners()Method, which you’ll use later.
  3. A service is responsible for retrieving and storing currency and exchange rate data.CurrencyServiceIs an abstract class whose concrete implementation is hidden outside the View Model. You can change different implementations at will.
  4. Any instance with access to this View Mode can access a list of currencies and pick a favorite from it. The UI uses this list to create an optional ListView.
  5. Called after a list of currencies has been obtained or a favorite currency has been modifiednotifyListeners()Method to issue a notification. The UI receives the notification and updates it.

There is another class in the Choose_favorites_viewModel. dart file: FavoritePresentation:

class FavoritePresentation {
  final String flag;
  final String alphabeticCode;
  final String longName;
  bool isFavorite;

  FavoritePresentation(
      {this.flag, this.alphabeticCode, this.longName, this.isFavorite,});
}
Copy the code

This class is for UI presentation. Try not to save anything unrelated to the UI.

In ChooseFavoritesViewModel, replace the loadData() method with the following code

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;
  }
Copy the code

LoadData gets a list of exchange rates. Then the _prepareChoicePresentation () method converts the list to the UI can directly display formats. _getFavoriteStatus() determines whether a currency is a favorite.

Next replace the toggleFavoriteStatus() method with the following code:

void toggleFavoriteStatus(int choiceIndex) {
    finalisFavorite = ! _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);
  }
Copy the code

Whenever this method is called, the View Model invokes the currency service to save the new favorite currency. Because notifyListeners are also called, the UI immediately shows the latest changes.

Congratulations, you have completed the View Model.

To sum up, all your View Model class needs to do is inherit the ChangeNotifier class and call the notifyListeners() method where the UI needs to be updated.

Services

We have three types of service: exchange rate, storage and network request. Looking at the architecture diagram below, all services are represented in the red box on the right:

  1. Create an abstract class to add all the methods you’ll need
  2. Write a concrete implementation class for an abstract class

Since each time a service is created the same way, we will use the network request as an example. The exchange rate service and storage service were already included in the initial project.

Create an abstract Service class

Open the web_api. Dart:

You should see the following code:

import 'package:moolax/business_logic/models/rate.dart';

abstract class WebApi {
  Future<List<Rate>> fetchExchangeRates();
}
Copy the code

This is an abstract class, so it doesn’t do anything concrete. However, it still reflects what the app needs it to do: it should request a string of exchange rates from the network. How you do that is up to you.

Using fake data

In web_API, create a new file, web_API_fake.dart. Then copy the following code:

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));returnlist; }}Copy the code

This class implements the abstract WebApi class and reverts some dead data. Now you can move on to the rest of the code, the network request part. When you’re ready to come back and make a real network request.

Add a Service locator

Even if the abstract classes are implemented, you still have to tell your app where to find the concrete implementation classes for those abstract classes.

There is a Service locator that can do this very quickly. A Service locator is an alternative to dependency injection. It can be used to decouple a service from the rest of the app.

There was a line in the ChooseFavoriatesViewModel:

final CurrencyService _currencyService = serviceLocator<CurrencyService>();
Copy the code

A serviceLocator is a singleton object that goes back to all the services you use.

In the Services directory, open service_locator. Dart. You should see the following code:

/ / 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());
}
Copy the code

Explanation:

  1. GetItIt’s a group calledget_itService location package. This has been pre-added topubspec.yaml.get_itAll registered objects are retained through a global singleton.
  2. This method is used to register the service. This method needs to be called before the UI is built.
  3. You can register your service as a lazy singleton. Registered as a singleton which means you’re getting the same instance back every time. A lazy singleton registered as a lazy singleton is initialized only when it is used for the first time.
  4. You can also register the View Model using the Service locator. This makes it easy to get their references in the UI. Of course the View Models are registered as a Factory. Every time I get back a new view Model instance.

Notice where the code calls setupServiceLocator(). Open the main.dart file:

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(), ); }}Copy the code

Registered FakeWebApi

Now sign up for FakeWebApi.

serviceLocator.registerLazySingleton<WebApi>(() => FakeWebApi());
Copy the code

Replace CurrencyServiceImpl with CurrencyServiceFake:

serviceLocator.registerLazySingleton<CurrencyService>(() => CurrencyServiceImpl());
Copy the code

CurrencyServiceFake was used in the initial project to make it work.

Introducing missing classes:

import 'web_api/web_api.dart';
import 'web_api/web_api_fake.dart';
import 'currency/currency_service_implementation.dart';
Copy the code

Run the app and tap the heart in the upper right corner.

Concrete implementation of Web API

A fake Web API implementation was registered earlier and the app is ready to run. Now you need to get the real data from the real Web server. In the services/ web_API directory, create a new file, web_API_implementation. dart. Add 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));
    }
    returnlist; }},Copy the code

Note the following points:

  1. asFakeWebApiThis class is also implementedWebApi. It contains the data fromapi.exchangeratesapi.ioLogic to get data. However, the rest of the app doesn’t know this, so if you want to switch to another Web API, this is undoubtedly the only place you can change it.
  2. Exchangeratesapi.io generously provides the exchangerate for the currency given the data, without additional tokens.

Open service_localtor.dart, change FakeWebApi() to WebApiImp(), and update the corresponding import statement.

import 'web_api/web_api_implementation.dart';

void setupServiceLocator() {
  serviceLocator.registerLazySingleton<WebApi>(() => WebApiImpl());
  // ...
}
Copy the code

To realize the Provider

Now it’s Provider’s turn. This is a Provider tutorial!

Since we’ve waited so long to start the Provider section, you should realize that a Provider is a very small part of an app. It is intended to facilitate passing values to child widgets when changes occur, but it is not an architecture or state management system.

Find the Provider package in pubspec.yaml:

dependencies:
  provider: ^4.01.
Copy the code

There is a special Provider: ChangeNotifierProvider. It listens for changes to the View Model implementing ChangeNotifier.

In UI/Views, open the choose_Favorites.dart file. The contents of this file are replaced 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.
}
Copy the code

You’ll find the buildListView() method, notice the following changes:

  1. Servie locator returns an instance of the View Model
  2. useStatefulWidgetIt containsinitState()Methods. Here you can tell the View Model to load the currency data.

Under the build() method, 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: () {
                  / / 5model.toggleFavoriteStatus(index); },),); },),),); }Copy the code

Code parsing:

  1. addChangeNotifierProvider, a special type of provider that listens for changes from the View Model.
  2. ChangeNotifierProviderThere is acreateMethods. This method provides the view Model value to the child Wdiget. You already have a reference to the View Model here, so just use it.
  3. Consumer, when the View ModelnotifyListeners()Tell you to rebuild the interface when changes occur. The Consumer builder method passes down the View Model value. This view Model is fromChangeNotifierProviderIt’s been handed down.
  4. usemodelTo rebuild the interface. Notice how little logic there is in the UI.
  5. Now that you have a reference to the View Model, you can call the methods inside it.toggleFavoriteStatus()Call thenotifyListeners().

Run the app again.

Use providers in large apps

You can add more interfaces as described in this article. Once you get used to adding View Models to every interface, consider creating base classes for certain classes to reduce repetitive code. This article doesn’t do that because it would take more time to understand the code.

Other architecture and state management methods

If you don’t like the architecture described in this article, consider the BLoC model. BLoC model introduction is also a good place to start. The BLoC pattern is not as hard to understand as it has been suggested.

There are others, but Provider and BLoC are by far the most common.