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

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

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

更新到最新版本

毫无疑问 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 参数。具体的缘由能够参考这个解答less

尽可能使用 StatelessWidget 替代 StatefulWidget

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

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

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

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

因为它是一个方法,没法直接在 Widget 树中调用,通常咱们放在 build 方法中,return 方法以前。工具

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

可是,因为 Provider 会监听 Value 的变化而更新整个 context 上下文,所以若是 build 方法返回的 Widget 过大过于复杂的话,刷新的成本是很是高的。那么咱们该如何进一步控制 Widget 的更新范围呢?oop

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

Widget build(BuildContext context) {
  return Container(child: MyText());
}

class MyText extends StatelessWidget {
  @override
  Widget 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(name)
);
复制代码

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).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 { 
  @override
  void 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

相关文章
相关标签/搜索