写点什么

使用 Provider 改造屎山代码,代码量降低了 2/3!

作者:岛上码农
  • 2022 年 5 月 19 日
  • 本文字数:3520 字

    阅读完需:约 12 分钟

使用 Provider 改造屎山代码,代码量降低了2/3!

前言

之前的几篇我们写了状态管理的机制和状态管理插件,接下来几篇我们就使用官方推荐的 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 页的数据。

  • 获取分页数据:根据当前页面和分页大小请求动态数据,并更新列表数据。

  • 预留deleteaddupdate 方法,以便后面的删除、添加和更新使用。


整个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 很臃肿,而且耦合度也变高。


@overridevoid 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进行改造,首先是将 DynamicPageStatefulWidget 改为 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 的局部刷新特性,也能够提高界面渲染的的性能。



发布于: 刚刚阅读数: 2
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
使用 Provider 改造屎山代码,代码量降低了2/3!_flutter_岛上码农_InfoQ写作社区