内容简介:这篇文章好的的地方在于它不仅讲了Flutter Provider如何管理State的,还讲述了一个Flutter App可以采用哪一种架构。这种架构是基于更加重要的一点,就是本文要讲述的Provider其实就是一种widget。搭配着最后,还是那句话要看原文的请到
这篇文章好的的地方在于它不仅讲了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是目前最普遍采用的。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 基于有限状态机的广告状态管理方案及实现
- 基于有限状态机的广告状态管理方案及实现
- Android实现底部状态栏切换
- 13行代码实现状态管理工具
- Apache FlinkCEP 实现超时状态监控
- 电商sku组合查询状态细究与实现
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。