这篇文章好的的地方在于它不仅讲了Flutter Provider如何管理State的,还讲述了一个Flutter App可以采用哪一种架构。这种架构是基于clean architecture和FilledStacks这两种架构原则的(这里可能理解或者表达有误,请指正)。但是文中最后采用的还是MVVM的模式。
更加重要的一点,就是本文要讲述的Provider其实就是一种widget。搭配着Consumer
这个widget一起使用,达到UI = f(state)这个state
变化,UI跟着变的效果。
最后,还是那句话要看原文的请到这里,文章本身有质量,而且写的不难。
正文
Flutter团队建议初学者使用Provider来管理state。但是Provider到底是什么,该如何使用?
Provider是一个UI工具。如果你对于架构、state和架构之间有疑惑,那么并不只有你是这样。本文会帮助你理清这些概念,让你知道如何从无到有写一个app。
本文会带你学习Provider管理state的方方面面。这里我们来写一个计算汇率的app,就叫做MoolaX。在写这个app的时候你会提升你的Flutter技能:
- app架构
- 实现一个Provider
- 熟练管理app的state
- 根据state的更改来更新UI
注意:本文假设你已经知道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来说十分有效。
使用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是需要的。卡片如下:
最后会用到Currency
和Rate
两个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();
}
}
解释:
- 使用
ChangeNotifier
来实现UI对view model的监听。这个类在Flutterfoundation
包。 - view model类继承了
ChangeNotifier
类。另一个选项是使用mixin。ChangeNotifier
里有一个notifyListeners()
方法,你后面会用到。 - 一个service来负责获取和保存货币以及汇率数据。
CurrencyService
是一个抽象类,它的具体实现隐藏在view model之外。你可以任意更换不同的实现。 - 任意可以访问这个view mode的实例都可以访问到一个货币列表,然后从里面选出一个最喜欢的。UI会使用这个列表来创建一个可选的listview。
- 在获取到货币列表或者修改了最喜欢的货币之后,都会调用
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,分别是:汇率交换,存储以及网络请求。看下面的架构图,所有服务都在右边红色的框表示:
- 创建一个抽象类,在里面添加所有会用到的方法
- 给抽象类写一个具体的实现类
因为每次创建一个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());
}
解释:
-
GetIt
是一个叫做get_it的service 定位包。这里已经预先添加到pubspec.yaml
里了。get_it会通过一个全局的单例来保留所有注册的对象。 - 这个方法就是用来注册服务的。在构建UI之前就需要调用这个方法了。
- 你可以把你的服务注册为延迟加载的单例。注册为单例也就是说你每次取回的是同一个实例。注册为一个延迟加载的单例等于,在第一次使用的时候,只有在用的时候才会初始化。
- 你也可以使用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;
}
},
注意下面的几点:
- 如同
FakeWebApi
,这个类也实现了WebApi
。它包含了从api.exchangeratesapi.io获取数据的逻辑。然而,app的其他部分并不知道这一点,所以如果你想换到别的web api,毫无疑问这里就是你唯一可以更改的地方。 - 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()
方法,注意如下的更改:
- servie定位器返回一个view model的实例
- 使用
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里只有很少的逻辑。 - 既然你有了view model的引用,那么完全可以调用里面的方法。
toggleFavoriteStatus()
调用了notifyListeners()
。
再次运行app。
在大型app中使用Provider
你可以按照本文所述的方式添加更多的界面。一旦你习惯了为每个界面添加view model就可以考虑为某些类创建基类来减少重复代码。本文没有这么做,因为这样的话理解这些代码要花更多的时间。
其他的架构和状态管理方法
如果你不喜欢本文所述的架构,可以考虑BLoC模式。BLoC模式入门也是一个很好的起点。你会发现BLoC模式也不像传说的那么难以理解。
还有其他的,不过Provider和BLoC是目前最普遍采用的。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。