本文主要是介绍善用 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 提供了两套方案方便我们及时释放资源。
- Provider 的默认构造方法中有一个 dispose 回调,会在 Provider 销毁时触发。我们只需要在这个回调中释放我们的资源即可。
Provider(create:(_) => Model(),dispose:(context, value) {// 释放资源}
)
复制代码
- 重写 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 重建。
最后是浓缩版的建议:
- 每一次通过 Provider 取值的时候都问自己一遍,我是否需要监听数据,还是只是单纯访问 Value 。
- 每一次通过 Provider 取值的时候都问自己一遍,是否可以用 Selector 替代 Consumer,不行的话是否可以用 Consumer 替代 Provider.of(context)。
provider
Flutter | 状态管理指南篇——Provider
作者:ZacJi
链接:https://juejin.im/post/5e3a93f0f265da57337cf29e
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这篇关于善用 Provider 榨干 Flutter 最后一点性能的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!