Flutter 入门与实战(五十四):Provider 之监听状态的局部变化

2,526 阅读4分钟

这是我参与8月更文挑战的第17天,活动详情查看:8月更文挑战

前言

在有些场景中,我们的组件只和状态管理的部分数据有关。举个例子,例如下面的动态详情界面。 image.png 底部有两个按钮:

  • 点赞按钮:点击按钮后会增加点赞数,再次点赞会取消点赞并减少点赞数;
  • 收藏按钮:点击按钮增加收藏数,再次点击会取消收藏并减少收藏数。

这个情况,如果我们使用 Providerwatch 方法监听状态对象的变化时,每次点击任意一个按钮都会导致整个界面刷新,这样不可避免会带来性能上的损耗。那有没有办法实现界面只监听与它有关的数据呢?本篇我们就来讲述如何实现监听状态的局部变化。

Provider 状态局部监听

Provider 提供了方法 R select<T, R>(R Function(T value) selector)来实现状态数据的局部变化,该方法接收一个 selector 函数参数,该函数的参数为 T,然后返回 R 类型值。也就是只监听状态数据的 R 部分数据,而不是监听全部数据。只有当 R 类型值发生改变时,才会通知依赖该数据的组件刷新,而其他不依赖该值的组件不会刷新。

详情页改造

我们新建一个 DynamicDetailModel 类作为详情页的状态管理,其中有三个状态数据:

  • _currentDynamic:当前动态详情数据,提供 get 属性供详情组件访问;
  • _praiseCount:点赞数,提供 get 属性供点赞按钮组件访问;
  • _favorCount:收藏数,提供 get 属性供收藏按钮组件访问;

同时增加了更新点赞数的方法 updatePraiseCount 和 更新收藏数方法 updateFavorCount,两个方法的逻辑如下:

  • updatePraiseCount:如果没有点赞则点赞数+1,如果点赞了,则点赞数-1。
  • updateFavorCount:如果没有收藏则收藏数+1,如果收藏了,则收藏数-1。

详情页面我们分成了三个大的组件,使用 DynamicDetailWrapper 包裹,以便三个组件给自管理自己的状态,其中两个按钮使用 Stack + Positioned 组件将按钮固定在页面底部。

class DynamicDetailWrapper extends StatelessWidget {
  final String id;
  const DynamicDetailWrapper({Key? key, required this.id}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider<DynamicDetailModel>(
        create: (context) => DynamicDetailModel(),
        child: Stack(
          children: [
            _DynamicDetailPage(id),
            Positioned(
                bottom: 0,
                height: 60,
                width: MediaQuery.of(context).size.width,
                child: Row(
                  mainAxisSize: MainAxisSize.min,
                  mainAxisAlignment: MainAxisAlignment.spaceAround,
                  children: [
                    _PraiseButton(),
                    _FavorButton(),
                  ],
                ))
          ],
        ));
  }
}

接下来就是三个组件各自使用 select 方法监听状态的局部数据。

// _DynamicDetailPage
Widget build(BuildContext context) {
    print('_DynamicDetailPage');
    DynamicEntity? currentDynamic =
        context.select<DynamicDetailModel, DynamicEntity?>(
      (dynamicDetail) => dynamicDetail.currentDynamic,
    );
    return Scaffold(
      appBar: AppBar(
        title: Text('动态详情'),
        brightness: Brightness.dark,
      ),
      body: currentDynamic == null
          ? Center(
              child: Text('请稍候...'),
            )
          : _getDetailWidget(currentDynamic),
    );
  }

  // ...省略其他代码
}

// _PraiseButton
class _PraiseButton extends StatelessWidget {
  const _PraiseButton({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    print('PraiseButton');
    return Container(
      alignment: Alignment.center,
      color: Colors.blue,
      child: TextButton(
        onPressed: () {
          context.read<DynamicDetailModel>().updatePraiseCount();
        },
        child: Text(
          context.select<DynamicDetailModel, String>(
            (DynamicDetailModel dyanmicDetail) {
              return '点赞 ' + dyanmicDetail.praiseCount.toString();
            },
          ),
          style: TextStyle(color: Colors.white),
        ),
        style: ButtonStyle(
            minimumSize: MaterialStateProperty.resolveWith((states) =>
                Size((MediaQuery.of(context).size.width / 2 - 1), 60))),
      ),
    );
  }
}

// _FavorButton 和_PraiseButton 类似,省略

三个组件对依赖的对象全部使用了 select方法替换了 watch 方法,并且每个组件都在 build 方法打印了各自的组件名称,以便验证是否只在其依赖的那部分状态数据发生改变时才刷新。

这里顺带说一个小坑,点赞按钮和收藏按钮一开始设置的宽度都是MediaQuery.of(context).size.width / 2,结果点击收藏按钮时会同时点击到点赞按钮,因此将其中一个按钮的宽度减了1(任意一个按钮都行),这应该是浮点数精度问题,导致两个按钮区域覆盖了,结果同时触发了两个按钮的点击事件。

运行结果

现在我们来看运行结果,对照模拟器界面操作和控制台日志,可以看到,点击任意一个按钮时只有按钮自身改变,其他不相关的组件没有调用 build 方法。

运行结果

从控制台打印的结果来看,点击按钮只会调用该按钮的 build 方法,其他组件没有重新 build,确实实现了局部刷新的效果。

总结

本篇通过把动态详情页分离为三个依赖状态不同数据的三个组件,使用 Provider 提供的 select 方法实现了状态局部监听。通过这个例子,其实给了我们状态管理的推荐做法,那就是尽量优先使用 select 方法做局部监听,这样在日后状态管理扩展后也能做到状态管理不同部分的界面更新分离,实现数据只驱动关联界面刷新,从而取得更好的性能。


我是岛上码农,微信公众号同名,这是Flutter 入门与实战的专栏文章,对应源码请看这里:Flutter 入门与实战专栏源码

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

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

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