善用 Provider 榨干 Flutter 最后一点性能

2023-12-18 14:48

本文主要是介绍善用 Provider 榨干 Flutter 最后一点性能,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Provider 作为 Google 钦定的状态管理框架,以其简单易上手的特点,成为大部分中小 App 的首选。Provider 的使用非常简单,官方文档也不长,基本上半个小时就能上手操作。但要用好 Provider 却不简单,这可关系到 App 的运行效率和流畅度。 下面我就总结了一些 Provider 在使用过程中需要注意的 Tips,帮助你榨干 Flutter 的最后一点性能!

⚠️ 提示:本文不是 Provider 入门教程,需要你对 Provider 有一个基本对了解。初学者建议跳转到问末首先阅读官方文档 & 实例教学。

更新到最新版本

毫无疑问 Flutter 连带整个第三方插件社区都在高密度的迭代,Provider 作为一个发布才1年多的库如今已经迭代到 4.0 了。每一次更新不仅仅是 Bug 的修复,还有大量功能的提升和性能的优化。比如 3.1 推出的 Selector,以及后期加入的针对性能的提示等。

正确地初始化 Provider

所有的 Provider 都有两个构造方法,分别为默认构造方法和便利构造方法。很多人简单地认为便利构造方法只是一种更加简便的构造方法,它们接收的参数是一样的。其实不对。 我们以 ChangeNotifierProvider 为例:

// ✅ 默认构造方法
ChangeNotifierProvider(create: (_) => MyModel(),child: ...
)
复制代码
// ❌ 默认构造方法
MyModel myModel;
ChangeNotifierProvider(create: (_) => myModel,child: ...
)
复制代码
// ✅ 便利构造方法
MyModel myModel;
ChangeNotifierProvider.value(value: myModel,child: ...
)
复制代码
// ❌ 便利构造方法
ChangeNotifierProvider.value(value: MyModel(),child: ...
)
复制代码

简单的说就是,如果你需要初始化一个新的 Value ,就使用默认构造方法,通过 create 方法的返回值传递。而如果你已经有了这个 Value 的实例,则使用便利构造方法,直接赋值给 value 参数。具体的原因可以参考这个解答。

尽量使用 StatelessWidget 替代 StatefulWidget

由于引入了 Provider 进行统一的状态管理,因此大部分 Widget 不再需要继承自 StatefulWidget 来更新数据了。StatelessWidget 的维护成本比 StatefulWidget 要低,构建效率更高。同时更少的代码量会让我们更容易地控制重建范围,提高渲染效率。

当然,对于部分需要依附于 Widget 生命周期的逻辑(比如首次进入页面进行 HTTP 请求),还是得继续使用 StatefulWidget 。

尽量使用 Consumer 替代 Provider.of(context)

Provider 取值有两种方式,一种是 Provider.of(context) ,直接返回 Value。

由于它是一个方法,无法直接在 Widget 树中调用,一般我们放在 build 方法中,return 方法之前。

Widget build(BuildContext context) {final text = Provider.of<String>(context);return Container(child: Text(text));
}
复制代码

但是,由于 Provider 会监听 Value 的变化而更新整个 context 上下文,因此如果 build 方法返回的 Widget 过大过于复杂的话,刷新的成本是非常高的。那么我们该如何进一步控制 Widget 的更新范围呢?

一个办法是将真正需要更新的 Widget 封装成一个独立的 Widget,将取值方法放到该 Widget 内部。

Widget build(BuildContext context) {return Container(child: MyText());
}class MyText extends StatelessWidget {@overrideWidget build(BuildContext context) {final text = Provider.of<String>(context);return Text(text);}
}
复制代码

另一个相对好一点的办法是使用 Builder 方法创造一个范围更小的 context。

Widget build(BuildContext context) {return Container(child: Builder(builder: (context) {final text = Provider.of<String>(context);return Text(text);}));
}
复制代码

这两种方法都能够在刷新 Widget 时跳过 Container 直接重建 Text 。无论哪种方法,其根本目的就是缩小 Provider.of(context) 中 context 的范围,减少 Widget 重建数量。但这两个方法都太过繁琐。

Consumer 是 Provier 的另一种取值方式,不同的是它是一个 Widget ,能够方便的嵌入到 Widget 树中调用,类似于上面的 Builder 方案。

Widget build(BuildContext context) {return Container(child: Consumer<String>(builder: (context, text, child) => Text(text),));
}
复制代码

Consumer 可以直接拿到 context 连带 Value 一并传作为参数传递给 builder ,使用无疑更加方便和直观,大大降低了开发人员对于控制刷新范围的工作成本。

Container 的 builder 方法有一个 child 属性,我们可以将 Container 层级下不受 Value 影响的 Widget 写到 child 中,这样当 Value 更新时不会重新构建 child 中的 Widget ,进一步提高效率。

Widget build(BuildContext context) {return Container(child: Consumer<String>(builder: (context, text, child) => Row(children: <Widget>[Text(text),child],),child: Text("不变的内容"),));
}
复制代码

上面代码中将不受 text 控制的 Text 放入 child 中并带入 builder 方法,这样当 text 改变时不会重新构建 child 中的 Text。

尽量使用 Selector 替代 Consumer

Selector 是 3.1 推出的功能,目的是更近一步的控制 Widget 的更新范围,将监听刷新的范围控制到最小。 实际项目中我们往往会根据业务场景或者页面元素来设计 Provider 的 Value,此时的 Value 其实就是 ViewModel。大量的数据都放入 Value 的后果就是,只要一个值的改动,就会触发整个 ViewModel 的 notifyListeners ,进而引发整个 ViewModel 关联 Widget 的刷新。

因此,我们需要一个能力,在执行刷新之前给我们一次机会,判断是否需要刷新,来避免不需要的刷新。这个能力,就是由 Selector 来实现的。

Selector<ViewModel, String>( selector: (context, viewModel) => viewModel.title,shouldRebuild: (pre, next) => pre != next,builder: (context, title, child) => Text(title)
);
复制代码

Selector 有两个范型参数,分别是 Provider 的 Value 类型以及 Value 中具体用到的参数类型。它有三个参数:

  • selector:是一个 Function,传入 Value ,要求我们返回 Value 中具体使用到的属性。
  • shouldRebuild:这个 Function 会传入两个值,其中一个为之前保持的旧值,以及此次由 selector 返回的新值,我们就是通过这个参数控制是否需要刷新 builder 内的 Widget。如果不实现 shouldRebuild ,默认会对 pre 和 next 进行深比较(deeply compares)。如果不相同,则返回 true。
  • builder:返回 Widget 的地方,第二个参数 title,就是我们刚才 selector 中返回的 String。

有了 Selector ,我们就可以避免 ViewModel 中一人改动全家更新的尴尬了。但 Selector 的使用场景远远不限于 ViewModel 这种重 Value ,即便是用在单一数据上,Selector 也能尽最大限度榨干性能。

比如一个数据列表 List ,如果修改其中一项数据,我们往往会更新整个 ListView 中的 ListTile 。

return ListView.builder(itemBuilder: (context, index) {final foo = Provider.of<ViewModel>(context).foos[index]return ListTile(title: Text(foo.didSelected),);
});
复制代码

如果通过 Performance 或者 Log 我们会发现,只修改 foos 中的某一个 foo 的 didSelected 属性,会将所有的 ListTile 都重新构建一遍。这无疑是没有必要的。

return ListView.builder(itemBuilder: (context, index) {return Selector< ViewModel, Foo>(selector: (context, viewModel) => viewModel.foos[index],shouldRebuild: (pre, next) => pre != next, // 此行可以省略builder: (context, foo, child) {return ListTile(title: Text(foo.didSelected),);},);
});
复制代码

通过 Selector 不仅能在构建 Widget 的过程中方便的获取 Value ,还能在构建子 Widget 之前留给我们一个额外的机会让我们决定是否需要重新构建子 Widget 。这样,ListView 每次就只会重构被修改的那个 ListTile 了。

善用 Provider.of(context) 的隐藏属性 listen

前面的 Consumer 似乎可以替代 Provider.of 的所有场景,那我们还需要 Provider.of 吗? 我们常常有这样的需求,就是只需要取得上层 Provider 的 Value,不需要监听并刷新数据,比如调用 Value 的方法。

Button(onPressed: () =>Provider.of<ViewModel>(context).run(),
)
复制代码

上面这样的写法会报错,因为 onPressed 方法只需要拿到 ViewModel 来调用 run 方法,它的内部不关心 ViewModel 是否有变化需不需要刷新。而 Provider.of 默认会监听 ViewModel 的改变并影响运行效率。 其实 Provider.of(context) 方法有一个隐藏属性 listen ,对于这种不关心 Value 是否变化只需要取值的情况,只需要将 listen 设置为 false(默认为 true ),Provider.of 返回的 Value 就不会触发监听刷新啦。

Button(onPressed: () =>Provider.of<ViewModel>(context, listen: false).run(),
)
复制代码

避免在错误的地方获取 Value

前面提到了,有些逻辑必须依赖 Widget 的生命周期,比如在进入页面时访问 Provider 。因此很多人会将逻辑放到 StatefulWidget 的 initState 或 didChangeDependencies 中。

initState() {super.initState();print(Provider.of<Foo>(context).value);
}
复制代码

但是这么做是有矛盾的,而且也会报错。既然将 load 方法放到了 initState 回调中,就意味着你希望该方法在 Widget 生命周期内只走一次,也就就意味着此处的 Value 并不关心值会不会改变。

因此,如果你只是想要拿到 Value 而不需要监听,直接使用上面的 listen 参数关闭监听即可。

initState() {super.initState();print(Provider.of<Foo>(context, listen: false).value);
}
复制代码

而如果你需要持续监听 Value 并作出反应,则不应该将逻辑放入 initState 中,didChangeDependencies 更适合这样的逻辑。但是由于 didChangeDependencies 会频繁调用多次,获取 Value 之后需要判断一下 Value 是否有改变,避免 didChangeDependencies 方法死循环。

Value value;didChangeDependencies() {super.didChangeDependencies();final value = Provider.of<Foo>(context).value;if (value != this.value) {this.value = value;print(value);}
}
复制代码

但是!

以上方案只使用于访问 Value ,如果需要修改 Value 并触发更新(例如访问网络),则会报错。因为 initState didChangeDependencies 中是不能触发状态更新的(包括调用 setState ),这样可能会导致 Widgets 在上次构建还没完成之前状态就又被更新,最终导致状态不统一。

因此,官方的建议是,如果 Provider Value 的方法不依赖外部参数,直接在 Value 初始化的时候执行方法。

class MyApi with ChangeNotifier {MyApi() {load();}Future<void> load() async {}
}
复制代码

如果 Provider Value 的方法必须依赖 Widgets 提供的外部参数,可以用 Future.microtask 将调用过程包在一个异步方法中。异步方法由于 event loop 的缘故会推迟到下一个周期运行,避开了冲突。

initState() {super.initState();Future.microtask(() =>Provider.of<MyApi>(context, listen: false).load(page: page););
}
复制代码

及时释放资源

及时释放不再使用的资源是优化的重点。Provider 提供了两套方案方便我们及时释放资源。

  1. Provider 的默认构造方法中有一个 dispose 回调,会在 Provider 销毁时触发。我们只需要在这个回调中释放我们的资源即可。
Provider(create:(_) => Model(),dispose:(context, value) {// 释放资源}
)
复制代码
  1. 重写 ChangeNotifier 的 dispose 方法。细心的同学可能会发现,ChangeNotifierProvider 的初始化方法中是没有 dispose 这个参数的,这是因为 ChangeNotifierProvider 会在销毁时自动帮我们调用 Value 的 dispose 方法。我们所需要做的,仅仅是重写 Value 的 dispose 方法罢了。
class Model with ChangeNotifier { @overridevoid dispose() {// 释放资源super.dispose();}
}
复制代码

其实,这恰恰也是 ChangeNotifierProvider 和 ListenableProvider 的最大区别。ChangeNotifierProvider 继承自 ListenableProvider ,只不过 ChangeNotifierProvider 对 Value 的类型要求更高,必须实现 ChangeNotifier ,而 dispose 是 ChangeNotifier 的一个方法。

除此之外,我们还应该避免将所有 Provider 状态都放置到顶层。虽然取用起来比较方便,但全局的 Provider 资源都无法释放,对性能的影响会越来越大。我们应该在构建新页面和新功能的时候就理清业务,让 Provider 只覆盖它所负责的范围,并在退出该功能页面后及时释放资源。

多打 Log 多跑 Performance

最简单最无脑的方式就是在 Widget 之间插入 Log 来观察 Widget 的刷新范围,一旦发现刷新范围过大,和实际逻辑不符就应该尝试查找优化点。这种排查方式虽然相对粗旷,但对于尚未怎么优化的项目而言效果显著。

对于已经做过初步优化的项目而言,如果还想近一步榨干 Flutter 的性能,就只能通过跑 Performance 搭配工具来分析出性能瓶颈。

总结

其实上面所有的 Tips ,背后其实都在做一件事情:减少 Widget 的重建。

虽然我们知道 Flutter 在内部做了大量高效的算法和策略来避免无效的重建和渲染,但再高效的算法也是有成本的,更何况算法对我们来说是一个黑盒子,我们无法保证它能一直有效,因此我们需要在源头就掐断无用的 Widget 重建。

最后是浓缩版的建议:

  1. 每一次通过 Provider 取值的时候都问自己一遍,我是否需要监听数据,还是只是单纯访问 Value 。
  2. 每一次通过 Provider 取值的时候都问自己一遍,是否可以用 Selector 替代 Consumer,不行的话是否可以用 Consumer 替代 Provider.of(context)。

provider

Flutter | 状态管理指南篇——Provider


作者:ZacJi
链接:https://juejin.im/post/5e3a93f0f265da57337cf29e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

这篇关于善用 Provider 榨干 Flutter 最后一点性能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/508797

相关文章

Vue3 的 shallowRef 和 shallowReactive:优化性能

大家对 Vue3 的 ref 和 reactive 都很熟悉,那么对 shallowRef 和 shallowReactive 是否了解呢? 在编程和数据结构中,“shallow”(浅层)通常指对数据结构的最外层进行操作,而不递归地处理其内部或嵌套的数据。这种处理方式关注的是数据结构的第一层属性或元素,而忽略更深层次的嵌套内容。 1. 浅层与深层的对比 1.1 浅层(Shallow) 定义

性能测试介绍

性能测试是一种测试方法,旨在评估系统、应用程序或组件在现实场景中的性能表现和可靠性。它通常用于衡量系统在不同负载条件下的响应时间、吞吐量、资源利用率、稳定性和可扩展性等关键指标。 为什么要进行性能测试 通过性能测试,可以确定系统是否能够满足预期的性能要求,找出性能瓶颈和潜在的问题,并进行优化和调整。 发现性能瓶颈:性能测试可以帮助发现系统的性能瓶颈,即系统在高负载或高并发情况下可能出现的问题

性能分析之MySQL索引实战案例

文章目录 一、前言二、准备三、MySQL索引优化四、MySQL 索引知识回顾五、总结 一、前言 在上一讲性能工具之 JProfiler 简单登录案例分析实战中已经发现SQL没有建立索引问题,本文将一起从代码层去分析为什么没有建立索引? 开源ERP项目地址:https://gitee.com/jishenghua/JSH_ERP 二、准备 打开IDEA找到登录请求资源路径位置

黑神话,XSKY 星飞全闪单卷性能突破310万

当下,云计算仍然是企业主要的基础架构,随着关键业务的逐步虚拟化和云化,对于块存储的性能要求也日益提高。企业对于低延迟、高稳定性的存储解决方案的需求日益迫切。为了满足这些日益增长的 IO 密集型应用场景,众多云服务提供商正在不断推陈出新,推出具有更低时延和更高 IOPS 性能的云硬盘产品。 8 月 22 日 2024 DTCC 大会上(第十五届中国数据库技术大会),XSKY星辰天合正式公布了基于星

Flutter 进阶:绘制加载动画

绘制加载动画:由小圆组成的大圆 1. 定义 LoadingScreen 类2. 实现 _LoadingScreenState 类3. 定义 LoadingPainter 类4. 总结 实现加载动画 我们需要定义两个类:LoadingScreen 和 LoadingPainter。LoadingScreen 负责控制动画的状态,而 LoadingPainter 则负责绘制动画。

从状态管理到性能优化:全面解析 Android Compose

文章目录 引言一、Android Compose基本概念1.1 什么是Android Compose?1.2 Compose的优势1.3 如何在项目中使用Compose 二、Compose中的状态管理2.1 状态管理的重要性2.2 Compose中的状态和数据流2.3 使用State和MutableState处理状态2.4 通过ViewModel进行状态管理 三、Compose中的列表和滚动

PR曲线——一个更敏感的性能评估工具

在不均衡数据集的情况下,精确率-召回率(Precision-Recall, PR)曲线是一种非常有用的工具,因为它提供了比传统的ROC曲线更准确的性能评估。以下是PR曲线在不均衡数据情况下的一些作用: 关注少数类:在不均衡数据集中,少数类的样本数量远少于多数类。PR曲线通过关注少数类(通常是正类)的性能来弥补这一点,因为它直接评估模型在识别正类方面的能力。 精确率与召回率的平衡:精确率(Pr

Flutter Button使用

Material 组件库中有多种按钮组件如ElevatedButton、TextButton、OutlineButton等,它们的父类是于ButtonStyleButton。         基本的按钮特点:         1.按下时都会有“水波文动画”。         2.onPressed属性设置点击回调,如果不提供该回调则按钮会处于禁用状态,禁用状态不响应用户点击。

SQL2005 性能监视器计数器错误解决方法

【系统环境】 windows 2003 +sql2005 【问题状况】 用户在不正当删除SQL2005后会造成SQL2005 性能监视器计数器错误,如下图 【解决办法】 1、在 “开始” --> “运行”中输入 regedit,开启注册表编辑器,定位到 [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVer

Linux性能分析工具合集

Linux性能分析工具合集 工具合集主要包含以下各种工具,对于了解Linux系统结构、网络结构、内核层次具有一定的帮助。 Linux Performance Observability ToolsLinux Static Performance ToolsLinux Performance Benchmark ToolsLinux Performance Tuning ToolsLinux