前言
實在是抱歉,最近專案太忙,所以更新的太慢了。廢話不多說,我們開始吧。
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
系列的第三篇,內容依然很簡單,而我又要說時間有限了。
未完待續。。。 期待不期待你說了算。