這篇文章好的的地方在於它不只講了Flutter Provider如何管理State的,還講述了一個Flutter App能夠採用哪種架構。這種架構是基於clean architecture和FilledStacks這兩種架構原則的(這裏可能理解或者表達有誤,請指正)。可是文中最後採用的仍是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技能:服務器
注意:本文假設你已經知道Dart和如何寫一個Flutter的app了。若是在這方面還有不清楚的話請移步 Flutter入門。
點擊「下載材料」來下載項目的代碼。而後你就能夠一步一步的跟着本文添加代碼完成開發。網絡
本文使用了Android Studio,可是Visual Studio Code也是能夠用的。(其實VS Code更好用,譯者觀點)。數據結構
在MoolaX裏你能夠選擇不一樣的貨幣。App運行起來是這樣的:架構
打開初始項目,解壓後的starter目錄。Android Studio會出現一個彈出框,點擊Get dependencies。app
在初始項目裏已經包含了一部分代碼,本教程會帶着你添加必要的代碼,讓你輕鬆學會下文的內容。
如今這個app運行起來的時候是這樣的:
若是你沒據說過clean architecture,再繼續以前請閱讀這篇文章。
主旨就是把核心業務邏輯從UI、數據庫、網絡請求和第三方包中分離出來。爲何?核心業務邏輯相對並不會那麼頻繁的更改。
UI不該該直接請求網絡。也不該該把數據庫讀寫的代碼寫的處處都是。全部的數據都應該從一個統一的地方發出,這就是業務邏輯。
這就造成了一個插件系統。即便你更換了一個數據庫,app的其餘部分也不會有任何的感知。你能夠從一個移動端UI更換的一個桌面UI,app的其餘部分也並不用關心。這對於開發一個易於維護、擴展的app來講十分有效。
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的架構基本是這樣的:
注意以下幾點:
理論部分到此結束,如今開始代碼部分!
項目的目錄結構以下:
咱們來看看mdels目錄:
這些就是業務邏輯要用到的數據結構了。類職責協同卡片模型是一個很好的方法能夠肯定哪些model是須要的。卡片以下:
最後會用到Currency
和Rate
兩個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(); } }
解釋:
ChangeNotifier
來實現UI對view model的監聽。這個類在Flutterfoundation
包。ChangeNotifier
類。另外一個選項是使用mixin。ChangeNotifier
裏有一個notifyListeners()
方法,你後面會用到。CurrencyService
是一個抽象類,它的具體實現隱藏在view model以外。你能夠任意更換不一樣的實現。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()
方法。
咱們這裏有三種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
類,反回了某些寫死的數據。如今你能夠繼續編寫其餘部分的代碼了,網絡請求的部分能夠放心了。何時準備好了,能夠回來實現真正的網絡請求。
即便抽象類都實現了,你仍是要告訴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()); }
解釋:
GetIt
是一個叫作get_it的service 定位包。這裏已經預先添加到pubspec.yaml
裏了。get_it會經過一個全局的單例來保留全部註冊的對象。注意代碼是在哪裏調用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
。
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實現,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; } },
注意下面的幾點:
FakeWebApi
,這個類也實現了WebApi
。它包含了從api.exchangeratesapi.io獲取數據的邏輯。然而,app的其餘部分並不知道這一點,因此若是你想換到別的web api,毫無疑問這裏就是你惟一能夠更改的地方。打開service_localtor.dart,把FakeWebApi()
修改成WebApiImp()
,並更新對應的import語句。
import 'web_api/web_api_implementation.dart'; void setupServiceLocator() { serviceLocator.registerLazySingleton<WebApi>(() => WebApiImpl()); // ... }
如今總算輪到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()
方法,注意以下的更改:
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); }, ), ); }, ), ), ); }
代碼解析:
ChangeNotifierProvider
,一個特殊類型的provider,它監聽了來自view model的修改。ChangeNotifierProvider
有一個create
方法。這個方法給子wdiget提供了view model值。在這裏你已經有了view model的引用,那就直接使用。Consumer
,當view model的notifyListeners()
告知更改發生的時候從新build界面。Consumer的builder方法向下傳遞了view model值。這個view model是從ChangeNotifierProvider
傳下來的。model
裏的數據來從新build界面。注意UI裏只有不多的邏輯。toggleFavoriteStatus()
調用了notifyListeners()
。再次運行app。
你能夠按照本文所述的方式添加更多的界面。一旦你習慣了爲每一個界面添加view model就能夠考慮爲某些類建立基類來減小重複代碼。本文沒有這麼作,由於這樣的話理解這些代碼要花更多的時間。
若是你不喜歡本文所述的架構,能夠考慮BLoC模式。BLoC模式入門也是一個很好的起點。你會發現BLoC模式也不像傳說的那麼難以理解。
還有其餘的,不過Provider和BLoC是目前最廣泛採用的。