前言
实在是抱歉,最近项目太忙,所以更新的太慢了。废话不多说,我们开始吧。
Selector
读文档
其实我本来是没有计划说说Selector的,但有朋友想让我介绍一下,所以先从Selector开始。
总得来说,Selector和Consumer是等价的,也是通过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就刷新,如果是collection,selector进行深度比较。- 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.bar或foo.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中的泛型A和S都是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系列的第三篇,内容依然很简单,而我又要说时间有限了。
未完待续。。。 期待不期待你说了算。