Flutter 入门与实战(四十七):使用 Provider 改造💩一样的代码,代码量降低了2/3!

4,349 阅读6分钟

这是我参与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 的方式更新界面的开发者会被评为“**草包**”了!

image.png

之前代码一看就很乱,首先是在列表里包括了添加、编辑、删除的回调代码,是想要是业务复杂一点,岂不是回调要满屏飞了!其次是业务代码和 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 很臃肿,而且耦合度也变高。
@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进行改造,首先是将 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

image.png

当然,代码减少是因为将业务代码抽离了,但是业务代码本身是可以复用的。下一篇我们将删除、添加和编辑完成后,再来看 Provider 如何进一步提高代码复用性和简化页面代码。

总结

通过 Provider 状态管理,得到的最大的好处其实是 UI 层和业务层代码分离,精简了 UI层代码的同时,也提高了业务代码的复用性。而 Provider 的局部刷新特性,也能够提高界面渲染的的性能。


我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章。

👍🏻:觉得有收获请点个赞鼓励一下!

🌟:收藏文章,方便回看哦!

💬:评论交流,互相进步!