[譯]Flutter - 使用Provider實現狀態管理

這篇文章好的的地方在於它不只講了Flutter Provider如何管理State的,還講述了一個Flutter App能夠採用哪種架構。這種架構是基於clean architectureFilledStacks這兩種架構原則的(這裏可能理解或者表達有誤,請指正)。可是文中最後採用的仍是MVVM的模式。html

更加劇要的一點,就是本文要講述的Provider其實就是一種widget。搭配着Consumer這個widget一塊兒使用,達到UI = f(state)這個state變化,UI跟着變的效果。web

最後,仍是那句話要看原文的請到這裏,文章自己有質量,並且寫的不難。數據庫

正文

Flutter團隊建議初學者使用Provider來管理state。可是Provider究竟是什麼,該如何使用?json

Provider是一個UI工具。若是你對於架構、state和架構之間有疑惑,那麼並不僅有你是這樣。本文會幫助你理清這些概念,讓你知道如何從無到有寫一個app。api

本文會帶你學習Provider管理state的方方面面。這裏咱們來寫一個計算匯率的app,就叫作MoolaX。在寫這個app的時候你會提高你的Flutter技能:服務器

  1. app架構
  2. 實現一個Provider
  3. 熟練管理app的state
  4. 根據state的更改來更新UI
注意:本文假設你已經知道Dart和如何寫一個Flutter的app了。若是在這方面還有不清楚的話請移步 Flutter入門

開始

點擊「下載材料」來下載項目的代碼。而後你就能夠一步一步的跟着本文添加代碼完成開發。網絡

本文使用了Android Studio,可是Visual Studio Code也是能夠用的。(其實VS Code更好用,譯者觀點)。數據結構

在MoolaX裏你能夠選擇不一樣的貨幣。App運行起來是這樣的:架構

最終效果

打開初始項目,解壓後的starter目錄。Android Studio會出現一個彈出框,點擊Get dependenciesapp

在初始項目裏已經包含了一部分代碼,本教程會帶着你添加必要的代碼,讓你輕鬆學會下文的內容。

如今這個app運行起來的時候是這樣的:

搭建App的架構

若是你沒據說過clean architecture,再繼續以前請閱讀這篇文章。

主旨就是把核心業務邏輯從UI、數據庫、網絡請求和第三方包中分離出來。爲何?核心業務邏輯相對並不會那麼頻繁的更改。

UI不該該直接請求網絡。也不該該把數據庫讀寫的代碼寫的處處都是。全部的數據都應該從一個統一的地方發出,這就是業務邏輯。

這就造成了一個插件系統。即便你更換了一個數據庫,app的其餘部分也不會有任何的感知。你能夠從一個移動端UI更換的一個桌面UI,app的其餘部分也並不用關心。這對於開發一個易於維護、擴展的app來講十分有效。

使用Provider管理state

MoolaX的架構就符合這個原則。業務邏輯處理匯率相關的計算。Local Storage、網絡請求和Flutter的UI、Provider這些所有都互相獨立。

Local storage使用的是shared preferences,可是這個和app的其餘部分沒有關聯。同理網絡請求如何獲取數據和app的其餘部分也沒有任何關聯。

接下來要理解的是UI、Flutter和Provider都在同一個部分裏。Flutter就是一個UI框架,Provider是這個框架裏的一個widget。

Provider是架構嗎?不是。
Provider是狀態管理嗎?不是,至少在這個app裏不是。

state是app的變量的當前值。這些變量是app的業務邏輯的一部分,分散、管理在不一樣的model對象裏。因此,業務邏輯管理了state,而不是Provider。

因此,Provider究竟是什麼呢?

它是狀態管理的helper,它是一個widget。經過這個widget能夠把model對象傳遞給它的子widget。

Consumer widget,屬於Provider 包的一部分,監聽了Provider暴露的mode值的改變,並從新build它的所有子widget。

使用Provider管理state系列對state和provider作了更加全面的解析。Provider有不少種,不過多數不在本文的範圍內。

和業務邏輯通訊

文本的架構模式受到了FilledStacks的啓發。它可讓架構足夠有條理而又不會太過複雜。對初學者也很友好。

這個模型很是相似於MVVM(Model View ViewModel)。

model就是從數據庫或者網絡請求獲得的數據。view就是UI,也能夠是一個screen或者widget。viewmodel就是在UI和數據中間的業務邏輯,並提供了UI能夠展現的數據。可是它對UI並沒有感知。這一單和MVP不一樣。viewmodel也不該該知道數據從哪裏來。

在MoolaX裏,每頁都有獨立的view model。數據能夠從網絡和本地存儲得到。處理這部份內容的類叫作services。MoolaX的架構基本是這樣的:

注意以下幾點:

  • UI頁面監聽view model的改變,也會給view model發送事件
  • view model不會感知到UI的具體細節
  • 業務邏輯與貨幣抽象交互。它不會感知數據是從網絡請求得來仍是從本地存儲得來。

理論部分到此結束,如今開始代碼部分!

建立核心業務邏輯

項目的目錄結構以下:

Models

咱們來看看mdels目錄:

這些就是業務邏輯要用到的數據結構了。類職責協同卡片模型是一個很好的方法能夠肯定哪些model是須要的。卡片以下:

最後會用到CurrencyRate兩個model。他們表明了先進和匯率,就算你沒喲計算機也須要這兩個。

View Model

view mode的職責就是拿到數據,而後轉化成UI可用的格式。

展開view_models目錄。你會看到兩個view model,一個是給結算頁用的,一個是給選擇匯率頁用的。

打開choose_favorites_viewmodel.dart。你會看到下面的代碼:

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

解釋:

  1. 使用ChangeNotifier來實現UI對view model的監聽。這個類在Flutterfoundation包。
  2. view model類繼承了ChangeNotifier類。另外一個選項是使用mixin。ChangeNotifier裏有一個notifyListeners()方法,你後面會用到。
  3. 一個service來負責獲取和保存貨幣以及匯率數據。CurrencyService是一個抽象類,它的具體實現隱藏在view model以外。你能夠任意更換不一樣的實現。
  4. 任意能夠訪問這個view mode的實例均可以訪問到一個貨幣列表,而後從裏面選出一個最喜歡的。UI會使用這個列表來建立一個可選的listview。
  5. 在獲取到貨幣列表或者修改了最喜歡的貨幣以後,都會調用notifyListeners()方法發出通知。UI會接受到通知,並做出更新。

choose_favorites_viewmodel.dart文件還有另外的一個類:FavoritePresentation:

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

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

這個類就是爲UI展現用的。這裏儘可能不保存任何與UI無關的內容。

ChooseFavoritesViewModel,用下面的代碼替換掉loadData()方法

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;
  }

loadData獲取一列匯率。接着,_prepareChoicePresentation()方法把列表轉化成UI能夠直接顯示的格式。_getFavoriteStatus()決定了一個貨幣是否爲最喜歡貨幣。

接着使用下面的代碼替換掉toggleFavoriteStatus()方法:

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);
  }

只要這個方法被調用,view model就會調用貨幣服務保存新的最喜歡貨幣。同時由於notifyListeners方法也被調用了,因此UI也會馬上顯示最新的修改。

恭喜你,你已經完成了view model了。

總結一下,你的view model類須要作的就是繼承ChangeNotifier類並在須要更新UI的地方調用notifyListeners()方法。

Services

咱們這裏有三種service,分別是:匯率交換,存儲以及網絡請求。看下面的架構圖,全部服務都在右邊紅色的框表示:

  1. 建立一個抽象類,在裏面添加全部會用到的方法
  2. 給抽象類寫一個具體的實現類

由於每次建立一個service的方式都差很少,咱們就用網絡請求爲例。初始項目中已經包含了匯率服務存儲服務了。

建立一個抽象service類

打開web_api.dart

你會看到以下的代碼:

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

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

這是一個抽象類,因此它並不具體作什麼。然而,它仍是會反映出app須要它作什麼:它應該從網絡請求一串匯率回來。具體如何實現由你決定。

使用假數據

web_api裏,新建一個文件web_api_fake.dart。以後複製以下代碼進去:

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;
  }

}

這個類實現了抽象WebApi類,反回了某些寫死的數據。如今你能夠繼續編寫其餘部分的代碼了,網絡請求的部分能夠放心了。何時準備好了,能夠回來實現真正的網絡請求。

添加一個Service定位器

即便抽象類都實現了,你仍是要告訴app去哪裏找這些抽象類的具體實現類。

有一個service定位器能夠很快完成這個功能。一個service定位器是一個依賴注入的替代。它能夠用來把一個service和app的其餘部分解耦。

ChooseFavoriatesViewModel裏有這麼一行:

final CurrencyService _currencyService = serviceLocator<CurrencyService>();

serviceLocator是一個單例對象,它回到你用到的全部的service。

services目錄下,打開service_locator.dart。你會看到下面的代碼:

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

解釋:

  1. GetIt是一個叫作get_it的service 定位包。這裏已經預先添加到pubspec.yaml裏了。get_it會經過一個全局的單例來保留全部註冊的對象。
  2. 這個方法就是用來註冊服務的。在構建UI以前就須要調用這個方法了。
  3. 你能夠把你的服務註冊爲延遲加載的單例。註冊爲單例也就是說你每次取回的是同一個實例。註冊爲一個延遲加載的單例等於,在第一次使用的時候,只有在用的時候纔會初始化。
  4. 你也可使用service定位器來註冊view model。這樣在UI裏能夠很容易拿到他們的引用。固然view models都是註冊爲一個factory了。每次取回來的都是一個新的view model實例。

注意代碼是在哪裏調用setupServiceLocator()的。打開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(),
    );
  }
}

註冊FakeWebApi

如今來註冊FakeWebApi

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

使用CurrencyServiceImpl替換CurrencyServiceFake

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

初始項目裏使用了CurrencyServiceFake,這樣才能運行起來。

引入缺失的類:

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

運行app,點擊右上角的心形。

Web API的具體實現

前面註冊了假的web api實現,app已經能夠運行了。下面就須要從真的web服務器上獲取真正的數據了。在services/web_api目錄下,新建文件web_api_implementation.dart。添加以下的代碼:

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;
  }
},

注意下面的幾點:

  1. 如同FakeWebApi,這個類也實現了WebApi。它包含了從api.exchangeratesapi.io獲取數據的邏輯。然而,app的其餘部分並不知道這一點,因此若是你想換到別的web api,毫無疑問這裏就是你惟一能夠更改的地方。
  2. exchangeratesapi.io慷慨的提供了給定數據的貨幣的匯率,都不要額外的token。

打開service_localtor.dart,把FakeWebApi()修改成WebApiImp(),並更新對應的import語句。

import 'web_api/web_api_implementation.dart';

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

實現Provider

如今總算輪到Provider了。這篇怎麼說也是一個Provider的教程!

咱們等了這麼久纔開始Provider的部分,你應該意識到了Provider實際上是一個app的很小一部分。它只是用來方便在更改發生的時候方便把值傳遞給子widget,但也不是架構或者狀態管理的系統。

pubspec.yaml裏找到Provider包:

dependencies:
  provider: ^4.0.1

有一個比較特殊的Provider:ChangeNotifierProvider。它監聽實現了ChangeNotifier的view model的修改。

ui/views目錄下,打開choose_favorites.dart文件。這個文件的內容替換爲以下的代碼:

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.
}

你會發現buildListView()方法,注意以下的更改:

  1. servie定位器返回一個view model的實例
  2. 使用StatefulWidget,它包含了initState()方法。這裏你能夠告訴view model加載貨幣數據。

build()方法下,添加以下的buildListView()實現:

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);
                },
              ),
            );
          },
        ),
      ),
    );
  }

代碼解析:

  1. 添加ChangeNotifierProvider,一個特殊類型的provider,它監聽了來自view model的修改。
  2. ChangeNotifierProvider有一個create方法。這個方法給子wdiget提供了view model值。在這裏你已經有了view model的引用,那就直接使用。
  3. Consumer,當view model的notifyListeners()告知更改發生的時候從新build界面。Consumer的builder方法向下傳遞了view model值。這個view model是從ChangeNotifierProvider傳下來的。
  4. 使用model裏的數據來從新build界面。注意UI裏只有不多的邏輯。
  5. 既然你有了view model的引用,那麼徹底能夠調用裏面的方法。toggleFavoriteStatus()調用了notifyListeners()

再次運行app。

在大型app中使用Provider

你能夠按照本文所述的方式添加更多的界面。一旦你習慣了爲每一個界面添加view model就能夠考慮爲某些類建立基類來減小重複代碼。本文沒有這麼作,由於這樣的話理解這些代碼要花更多的時間。

其餘的架構和狀態管理方法

若是你不喜歡本文所述的架構,能夠考慮BLoC模式。BLoC模式入門也是一個很好的起點。你會發現BLoC模式也不像傳說的那麼難以理解。

還有其餘的,不過Provider和BLoC是目前最廣泛採用的。

相關文章
相關標籤/搜索