Flutter状态管理:Provider4 入门教程(三)

5,375 阅读4分钟

前言

实在是抱歉,最近项目太忙,所以更新的太慢了。废话不多说,我们开始吧。

Selector

读文档

其实我本来是没有计划说说Selector的,但有朋友想让我介绍一下,所以先从Selector开始。

总得来说,SelectorConsumer是等价的,也是通过Provider.of获取数据的,不同的是,Selector正如他的名字一样,他会过滤掉一些不必要的数据更新从而阻止重新构建,也就是说Selector只会更新符合条件的数据。

我们先看一下Selector的定义:

class Selector<A, S> extends Selector0<S> {
  /// {@macro provider.selector}
  Selector({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector != null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(context, Provider.of(context)),
          child: child,
        );
}

先解释一下Selector<A, S>中的泛型:

  • A是我们从顶层获取的Provider的类型
  • S是我们关心的具体类型,也就是获取到的Provider中真正对我们有用的类型,需要在selector 中返回该类型。这个Selector的刷新范围也从整个Provider变成了 S。

快速地看一下Selector的中的属性:

  • selector:就是一个Function,入参会将我们获取的顶层 provider传入,然后再返回我们所关心的S
  • shouldRebuild:这个属性会储存selector过滤后的值,也就是selector返回的S 并拿收到通知之后新的S与缓存的S进行比较,以此来判断这个Selector是否需要重新构建,默认preview!=next就刷新,如果是collectionselector进行深度比较。
  • builder:和Consumer一样,这里返回的是要构建的控件,第二个参数provider,就是我们刚才selector中返回的S
  • child:这个用于优化一些不用刷新的部分,之前我们说Consumer的时候也有说过。

默认情况下,Selector中的builder是否会被调用更新取决于selector中新旧数据比较结果,如果新旧数据是collection,那么这个比较结果是通过collection包中的DeepCollectionEquality得出来的。

这个默认行为可以通过自定义shouldRebuild回调来实现重写。

注意:被选中的数据必须是不可变的(immutable),否则Selector可能会认为没有任何变化,因此不会再次调用builder。

所以, selector应该返回的是一个集合(List/Map/Set/Iterable)或者重写了==的类。

但是有时候我们并不想去重写==,实现同样效果最简单的方式是使用Tuple:

Selector<Foo, Tuple2<Bar, Baz>>(
  selector: (_, foo) => Tuple2(foo.bar, foo.baz),
  builder: (_, data, __) {
    return Text('${data.item1}  ${data.item2}');
  }
)

上面的例子中,只有foo.barfoo.bar发生变化时,builder才会被再次调用。

关于Tuple具体如何使用,大家可以自行学习。

举个例子

上面说了一堆无非是对官方文档的罗列,我们说说具体应用。

简单说一下我们要实现的功能,十分简单,有一个商品列表,当我们点击某个商品的时候,商品会显示加入购物车。这个功能其实很简单了,我们需要为商品Commodity设置一个是否被加入购物车的字段isSelected,然后当我们点击了商品时,我们要更新isSelected字段,此时我们必然会通知Flutter更新UI,如果使用的是ChangeNotifier,那就是调用用notifyListeners。这可以实现我们的需求,但仔细一想,如果用这种方式,那么所有依赖这个Provider的Commodity都会进行刷新,也就全列表进行更新,这真的有必要吗?

这个时候我们可以考虑使用Selector进行优化--过滤掉不必要的刷新。

首先,我们创建一个CommodityProvider

class CommodityProvider with ChangeNotifier {
  List<Commodity> _commodityList =
      List.generate(10, (index) => Commodity('Commodity Name_$index', false));

  get commodityList => _commodityList;

  get length => commodityList.length;

  addToCart(int index) {
    Commodity commodity = commodityList[index];
    commodityList[index] = Commodity(commodity.name, !commodity.isSelected);
    notifyListeners();
  }
}

Commodity这个实体类很简单了,就两个字段,一个是商品的名字name,另一个是标记是否加入了购物车isSelected。其中_commodityList在实际工作中一般来说是从服务器获取的,这里为了方便我们直接写死。而addToCart方法则就是从购物车中加入或者删除,当点击对应index的商品时,我们会将该商品添加到购物车或者从购物车中移除。我们要通过commodityList来渲染整个列表,而length则是商品列表的长度。

接下来我们要见证一下Selector是否真的可以过滤刷新。 接下来,我们还是要在顶层页面通过ChangeNotifierProvider提供数据。

class CommodityListScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ChangeNotifierProvider(
      create: (_)=>CommodityProvider(),
      child: ourWidget,
    );
  }
}

很显然,要想实现这个列表我们必然得知道列表的长度length,而length是作用于整个列表的,但是我们并不希望它因为列表中某个商品发生变化就刷新,所以现在我们要过滤掉全部刷新,通过Selector实现一个不刷新的“Consumer”。

    Selector<CommodityProvider, CommodityProvider>(
        shouldRebuild: (pre, next) => false,
        selector: (_, provider) => provider,
        builder: (context, provider, child) {
          print("build selector 1");
          return ourWidget;
        },
      ),

在这里,Selector中的泛型AS都是CommodityProvider,因为我们想要获取的是整个CommodityProvider,只不过我们把shouldRebuild重写了,从而避免不必要的刷新。

接下来我们实现一下我们的商品列表:

ListView.builder(
              itemCount: provider.length,
              itemBuilder: (BuildContext context, int index) =>
                  Selector<CommodityProvider, Commodity>(
                    selector:
                        (BuildContext context, CommodityProvider provider) =>
                            provider.commodityList[index],
                    builder: (BuildContext context, Commodity commodity,
                        Widget child) {
                      print("build item $index");
                      return ListTile(
                        onTap: () => provider.addToCart(index),
                        title: Text("${commodity.name}"),
                        trailing: Icon(commodity.isSelected
                            ? Icons.remove_shopping_cart
                            : Icons.add_shopping_cart),
                      );
                    },
                  ));
        }

我们可以看到这里的selector返回了provider.commodityList[index],也就是某一个具体的商品,所以每个商品只需要关心自己的一亩三分地就OK了,这样Selector的刷新范围就仅限于当前商品,与此同时我们在Selector<CommodityProvider, Commodity>builder里添加了日志以验证过滤刷新机制。

Come on!运行一下,随便点几个商品,然后看一下日志:

I/flutter (29438): build selector 1
I/flutter (29438): build item 0
I/flutter (29438): build item 1
I/flutter (29438): build item 2
I/flutter (29438): build item 3
I/flutter (29438): build item 4
I/flutter (29438): build item 5
I/flutter (29438): build item 6
I/flutter (29438): build item 7
I/flutter (29438): build item 8
I/flutter (29438): build item 9
I/flutter (29438): build item 7
I/flutter (29438): build item 5
I/flutter (29438): build item 4

怎么样?现在我们只刷新了我们点击的商品,从而避免了整个列表的刷新,我们又在性能优化的路上前进了一小步。

欲知后事请听下回分解

作为Provider系列的第三篇,内容依然很简单,而我又要说时间有限了。

未完待续。。。 期待不期待你说了算。