前言
实在是抱歉,最近项目太忙,所以更新的太慢了。废话不多说,我们开始吧。
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
系列的第三篇,内容依然很简单,而我又要说时间有限了。
未完待续。。。 期待不期待你说了算。