前言
之前的几篇我们写了状态管理的机制和状态管理插件,接下来几篇我们就使用官方推荐的 Provider 来改造旧的代码,你会发现改造前后具有十分大的差别。
Provider 简介
Provider 是 Flutter 一个入门级的状态管理插件,基于 InheritedWidget 实现。Provider 能够沿着组件树共享状态数据,示例代码如下:
Provider (
create: (_) => Model(),
child: someWidget(),
);
Provider类本身并不会在状态改变的时候自动更新子组件,因此更常用的是使用其子类:
ListenableProvider:监听实现了 Listenable 的的对象,并将其暴露给下级组件。当触发一个事件后会通知子组件依赖发生变化进而实现重建。
ChangeNotifierProvider:最为常用的一个方式,是ListenableProvider的子类。监听实现了 ChangeNotifier 接口的对象,当该对象调用 notifyListeners 的时候,就会通知全部的监听组件更新组件。
ValueListenableProvider:监听实现了ValueListenable接口的对象。当该对象改变时,会更新其下级组件。
StreamProvider:监听 Stream 对象,然后将其内容暴露给子组件。通常是向一个组件以流的方式提供大量的内容,例如电池电量监测、Firebase 查询等。
如果一个对象被多个组件共享,那么可以使用如下方式:
// 被多个组件共享的对象
MyChangeNotifier variable;
ChangeNotifierProvider.value(
value: variable,
child: ...
)
在 Widget 中使用状态数据有三种方式:
使用 context.read<T>() 方法:该方法返回 T 类型的状态数据对象,但不会监听该对象的改变,适用于只读的情况;
使用 context.watch<T>() 方法:该方法返回 T 类型状态数据对象,并且会监听它的变化,适用于需要根据状态更新的状况。
使用 context.select<T,R>(R cb(T value)) 方法:返回 T 对象中的 R 类型对象,这可以使得 Widget 只监听状态对象的部分数据。
详细内容建议大家去看 Provider 的官方文档,我们后续的篇章也会涉及其中的内容。
代码分析
我们在前面的篇章介绍了一个动态模块的管理,包括了整个 CRUD 过程。具体可以从专栏阅读之前网络请求相关的篇章。首先我们来改造一下列表的代码,回头再来看之前的代码,就会知道为什么说直接使用 setState 的方式更新界面的开发者会被评为“草包”了!
之前代码一看就很乱,首先是在列表里包括了添加、编辑、删除的回调代码,是想要是业务复杂一点,岂不是回调要满屏飞了!其次是业务代码和 UI 代码混用,一个是代码又臭又长——俗称💩一样的代码,另外一个是业务代码的复用性降低了。比如说,我们在别的地方可能也会用到动态的增改删查业务,总不能再复制、粘贴再来一遍吧?
代码改造
现在我们来使用Provider 将业务和 UI 分离。将业务相关的代码统一放到状态管理中,UI 这边只处理界面相关的代码。首先抽取一个 DynamicModel 类,文件名为 dynamic_model.dart,把列表的相关业务代码放进来:
列表数据:使用一个 List<DynamicEntity> 对象存储列表数据,默认为空数组。
分页数据:当前页码 _currentPage,固定每页大小为 20。
刷新方法:refresh,将当前页码置为 1,重新请求第一页数据。
加载方法:load,将当前页码加 1,请求第 N 页的数据。
获取分页数据:根据当前页面和分页大小请求动态数据,并更新列表数据。
预留delete、add 和 update 方法,以便后面的删除、添加和更新使用。
整个DynamicModel类的代码如下,这里关键的一点是使用 with ChangeNotifier 使得 DynamicModel 混入ChangeNotifer的特性,以便 ChangeNotifierProvider 能够为其添加监听器,并且在调用 notiferListeners的时候通知状态依赖的子组件进行更新。
// 堆代码 duidaima.com
class DynamicModel with ChangeNotifier {
List<DynamicEntity> _dynamics = [];
int _currentPage = 1;
final int _pageSize = 20;
List<DynamicEntity> get dynamics => _dynamics;
void refresh() {
_currentPage = 1;
_requestNewItems();
}
void load() {
_currentPage += 1;
_requestNewItems();
}
void _requestNewItems() async {
var response = await DynamicService.list(_currentPage, _pageSize);
if (response != null && response.statusCode == 200) {
List<dynamic> _jsonItems = response.data;
List<DynamicEntity> _newItems =
_jsonItems.map((json) => DynamicEntity.fromJson(json)).toList();
if (_currentPage == 1) {
_dynamics = _newItems;
} else {
_dynamics += _newItems;
}
}
notifyListeners();
}
void removeWithId(String id) {}
void add(DynamicEntity newDynamic) {}
void update() {}
}
接下来是使用 Provider 为动态模块提供状态管理,如前面的几章所述,Provider 需要处于组件的上级才能够为子组件提供状态共享,因此我们有两种方式来实现这种方式。在构建 DynamicPage列表页面的 app.dart 中将 DynamicPage 作为 Provider 的下级。如下所示,这种方式的缺点是因为这是首页,如果各个模块的代码都往这里对方,会使得 app.dart 很臃肿,而且耦合度也变高。
@override
void initState() {
super.initState();
_homeWidgets = [
ChangeNotifierProvider<DynamicModel>(
create: (context) => DynamicModel(),
child: DynamicPage(),
),
MessagePage(),
CategoryPage(),
MineSliverPage(),
];
}
使用一个 Widget 包裹 DynamicPage 以及 Provider来降低代码的耦合度,避免 app.dart 中的代码过于臃肿。
class DynamicWrapper extends StatelessWidget {
const DynamicWrapper({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => DynamicModel(),
child: DynamicPage(),
);
}
}
之后就是对 DynamicPage进行改造,首先是将 DynamicPage 由 StatefulWidget 改为 StatelessWidget,然后移除掉相关业务代码。,最后就是在 build 方法中从 Provider 获取界面所需的数据,或调用对应的方法。改造完的 DynamicPage 就十分清爽了,如下所示:
class DynamicPage extends StatelessWidget {
DynamicPage({Key key}) : super(key: key);
final EasyRefreshController _refreshController = EasyRefreshController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('动态', style: Theme.of(context).textTheme.headline4),
actions: [
IconButton(
icon: Icon(Icons.add),
onPressed: () {
RouterManager.router
.navigateTo(context, RouterManager.dynamicAddPath);
}),
],
brightness: Brightness.dark,
),
body: EasyRefresh(
controller: _refreshController,
firstRefresh: true,
onRefresh: () async {
context.read<DynamicModel>().refresh();
},
onLoad: () async {
context.read<DynamicModel>().load();
},
child: ListView.builder(
itemCount: context.watch<DynamicModel>().dynamics.length,
itemBuilder: (context, index) {
return DynamicItem(context.watch<DynamicModel>().dynamics[index],
(String id) {
context.read<DynamicModel>().removeWithId(id);
});
},
),
),
);
}
}
在 ListView.builder 中我们使用了 contxt.watch<DynamicModel>方法来获取最新的动态列表 ,从而使得当列表数据改变时能够刷新界面。而在调用方法方面,我们则使用了 context.read<DynamicModel>方法,因为这里并不需要监听状态的改变。运行一下,发现和之前的效果一样,改造完成。
改造前后对比
我们来对比改造前后的 DynamicPage 代码,如下图所示(左侧为旧代码)。可以看到,大部分代码都被移除了,实际原先的代码有120行,而现在的代码只有40行了,足足减少了 2/3!

当然,代码减少是因为将业务代码抽离了,但是业务代码本身是可以复用的。下一篇我们将删除、添加和编辑完成后,再来看 Provider 如何进一步提高代码复用性和简化页面代码。
总结
通过 Provider 状态管理,得到的最大的好处其实是 UI 层和业务层代码分离,精简了 UI 层代码的同时,也提高了业务代码的复用性。而 Provider 的局部刷新特性,也能够提高界面渲染的的性能。