这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战
前言
之前的几篇我们写了状态管理的机制和状态管理插件,接下来几篇我们就使用官方推荐的 Provider 来改造旧的代码,你会发现改造前后具有十分大的差别。关于 Provider
的示例,之前翻译了一篇官网推荐的购物车示例文章:Flutter 入门与实战(四十):以购物车为例初探状态管理。
Provider 简介
Provider 当前最新版本是5.0.0,使得组件树能够共享状态数据的方式为:
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 过程。具体可以从专栏:Flutter 入门与实战的第二十二到第二十七篇。首先我们来改造一下列表的代码,回头再来看之前的代码,就会知道为什么说直接使用 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
的时候通知状态依赖的子组件进行更新。
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 的局部刷新特性,也能够提高界面渲染的的性能。
我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章。
👍🏻:觉得有收获请点个赞鼓励一下!
🌟:收藏文章,方便回看哦!
💬:评论交流,互相进步!