前言
進入這個帖子,可以先看看前面的介紹哈。
Selector
介紹
selector.dart中有段關於Selector的介紹
/// {@template provider.selector}
....
/// {@endtemplate}
class Selector<A, S> extends Selector0<S> {
}
class Selector0<T> extends SingleChildStatefulWidget {
final ValueWidgetBuilder<T> builder;
final T Function(BuildContext) selector;
final ShouldRebuild<T> _shouldRebuild;
}
複製程式碼
我們大致領會到一下關鍵點。
- Selector相當於Cosumer,但是可以在某些值不變的情況下,防止rebuild。
- selector方法:Selector使用 Provider.of獲取共享的資料。資料作為selector方法的入參A,執行selector方法,返回build需要的資料S,返回的資料要儘可能少,能滿足build就好。
- shouldRebuild:預設判斷前後兩次S相等性,來決定是否rebuild。並且也提供了自定義的shouldRebuild方法來判斷,引數是前後兩次S。
- S:selector的資料,必須是immutable(不可變)的.因此 selector通常返回集合或覆蓋了"=="的類。如果需要selector多個值,推薦[tuple](pub.dev/packages/tu…)
immutable:一個類的物件在通過構造方法建立後如果狀態不會再被改變,那麼它就是一個不可變(immutable)類。它的所有成員變數的賦值僅在構造方法中完成,不會提供任何 setter 方法供外部類去修改。
//tuple舉例
Selector<Foo, Tuple2<Bar, Baz>>(
selector: (_, foo) => Tuple2(foo.bar, foo.baz),
builder: (_, data, __) {
return Text('${data.item1} ${data.item2}');
}
)
複製程式碼
使用舉例
接下來,是一個Selector的使用場景。 Demo倉庫地址
入口:main_provider.dart -- Selector使用
效果
頁面上有三個字母按鈕,統計每個按鈕點選次數。點選按鈕,則按鈕上數字+1。
程式碼
//首先我們有一個model,做資料處理,ChangeNotifier用到Provider中
class CountModel extends ChangeNotifier {
//key為字母,value為點選次數統計
Map<String, int> contentMap = SplayTreeMap();
//初始化資料
initData() {
contentMap["a"] = 0;contentMap["b"] = 0;contentMap["c"] = 0;
}
//增加字母按鈕的點選次數
increment(String content) {
contentMap[content] = contentMap[content] + 1;
//通知重新整理
notifyListeners();
}
}
class SelectorDemoWidget extends StatefulWidget {
SelectorDemoWidget({Key key}) : super(key: key);
@override
_SelectorDemoWidgetState createState() => _SelectorDemoWidgetState();
}
class _SelectorDemoWidgetState extends State<SelectorDemoWidget> {
CountModel _model;
@override
void initState() {
super.initState();
//初始化資料
_model = new CountModel()..initData();
}
@override
Widget build(BuildContext context) {
//構建一組字母按鈕(CountItemWidget)
List<CountItemWidget> _children = _model.contentMap.keys
.map((key) => CountItemWidget(content: key))
.toList();
return Scaffold(
appBar: AppBar(title: Text("Selector示例"),),
//ChangeNotifierProvider 常用方式
body: ChangeNotifierProvider.value(
value: _model,
child: ListView(children: _children),
));
}
}
//字母按鈕
class CountItemWidget extends StatelessWidget {
final String content;
CountItemWidget({this.content});
@override
Widget build(BuildContext context) {
print("CountItemWidget:build");
return Container(
height: 80,
padding: EdgeInsets.all(15),
alignment: Alignment.center,
child: RaisedButton(
onPressed: () =>
Provider.of<CountModel>(context, listen: false).increment(content),
child: Selector<CountModel, int>(
//從 CountModel得到對應字母的count
selector: (context, model) => model.contentMap[content],
//如果前後兩次的count不相等,則重新整理
shouldRebuild: (preCount, nextCount) => preCount != nextCount,
builder: (context, count, child) {
print("$content Selector:builder");
return Text("$content : $count");
}),
),
);
}
}
複製程式碼
原始碼分析
一個簡單的示例就完成啦。
我們來看看Selector內部的實現。
當更新S時,比較前後S值是否相同。 不相同,則重新構建並且快取住。相同,則使用cache的Widget
解讀
//省略了部分程式碼
typedef ShouldRebuild<T> = bool Function(T previous, T next);
class Selector<A, S> extends Selector0<S> {
Selector({
Key key,
@required ValueWidgetBuilder<S> builder,
//這部分程式碼重點就是selector,Function引數,如何從A->S
@required S Function(BuildContext, A) selector,
ShouldRebuild<S> shouldRebuild,
Widget child,
}) : assert(selector != null),
super(
key: key,
shouldRebuild: shouldRebuild,
builder: builder,
//寫成Provider.of<A>(context),更明確點
selector: (context) => selector(context, Provider.of(context)),
child: child,
);
}
class Selector0<T> extends SingleChildStatefulWidget {
final ValueWidgetBuilder<T> builder;
final T Function(BuildContext) selector;
final ShouldRebuild<T> _shouldRebuild;
}
class _Selector0State<T> extends SingleChildState<Selector0<T>> {
//selector方法 A -> S
T value;
//如果不重新build,返回cache Widget
Widget cache;
//oldWidget --> Selector
Widget oldWidget;
@override
Widget buildWithChild(BuildContext context, Widget child) {
//實際呼叫過程,我們從示例程式碼的角度來看: CountModel是A,int是S
//1.CountModel model=Provider.of<CountModel>(context);
//2.int count = model.contentMap[content];count即為selected
final selected = widget.selector(context);
//3.value==selected? value:preCount, selected:nextCount
var shouldInvalidateCache = oldWidget != widget
||(widget._shouldRebuild != null
&& widget._shouldRebuild.call(value, selected))
||(widget._shouldRebuild == null
&& !const DeepCollectionEquality().equals(value, selected));
//4.不相等,則cache需要失效。
if (shouldInvalidateCache) {
//5.生成當次的Widget,並cache住value和Widget
value = selected;
oldWidget = widget;
cache = widget.builder(
context,
selected,
child,
);
}
return cache;
}
}
複製程式碼
流程圖
通過上面的分析,我們得到一個流程圖
適用場景
Selector相較於 Cosumer,提供了更細粒度的重新整理控制。
某個元件只關心Model中某個資料變化,可以考慮用Selector,即整體與區域性。
特別需要指明的是selector的結果,必須是不可變的物件。 如果同一個物件,只是改變物件屬性,那shouldRebuild的兩個值永遠是相等的。
我們在 前一篇Provider開發應用 提到的點贊帖子重新整理單個的場景,同樣可以用到Selector
我們列出程式碼參考
class PostItemWidget2 extends StatelessWidget {
final PostBean post;
final void Function(BuildContext context, PostBean post) click;
const PostItemWidget2({Key key, this.post, this.click}) : super(key: key);
@override
Widget build(BuildContext context) {
print("PostItemWidget2:build");
return GestureDetector(
onTap: () => click?.call(context, post),
child: Container(
height: 80,
child: Row(
children: <Widget>[
Expanded(
child: Text(
"${post?.content}",
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
Container(
width: 50,
child: Selector<PostItemChange, bool>(
selector: (context, itemChange) => post.isLike,
shouldRebuild: (pre, next) => pre != next,
builder: (context, isLike, child) {
return Icon(
Icons.favorite,
color: (isLike ?? false) ? Colors.red : Colors.grey,
);
}),
),
],
),
));
}
}
複製程式碼
ItemRefresher
引入
正如 Selector庫所指出的
selector的結果,必須是不可變的物件
然而有時,UI層往往會直接使用資料層的物件,而不是轉成UI層物件。
例如我們的例子中,PostItemWidget始終對應同一個PostBean。 前後區別就在於 PostBean.isLike發生了變化。同時我們的重新整理Notify只是簡單的 PostNotifier(id),並不會包含PostBean的全貌。
可以參考Selector自定義一個Item Refresh。
下面這個可以參考
原始碼
//區別點 是否需要rebuild,是notifier與value結合判斷
typedef ShouldRebuild<A, T> = bool Function(A notifier, T value);
class ItemRefresher<A, T> extends SingleChildStatefulWidget {
final ShouldRebuild<A, T> _shouldRebuild;
final T value;
ItemRefresher({
Key key,
//區別點 value有初始值,
@required this.value,
ShouldRebuild<A, T> shouldRebuild,
@required this.builder,
Widget child,
}) : assert(builder != null),
this._shouldRebuild = shouldRebuild,
super(key: key, child: child);
final ValueWidgetBuilder<T> builder;
@override
_ItemRefresherState<A, T> createState() => _ItemRefresherState<A, T>();
}
class _ItemRefresherState<A, T> extends SingleChildState<ItemRefresher<A, T>> {
Widget cache;
Widget oldWidget;
@override
Widget buildWithChild(BuildContext context, Widget child) {
//邏輯與selector類似
A notifier = Provider.of(context);
var shouldInvalidateCache = oldWidget != widget ||
(widget._shouldRebuild != null &&
notifier != null &&
widget._shouldRebuild.call(notifier, widget.value));
if (shouldInvalidateCache) {
oldWidget = widget;
cache = widget.builder(
context,
widget.value,
child,
);
}
return cache;
}
}
複製程式碼
使用
定義一個PostNotifier
class PostNotifier with ChangeNotifier {
int id;
}
複製程式碼
構建PostItemWidget
Widget _buildListItem(BuildContext context, PostBean post) {
return ItemRefresher<PostNotifier, PostBean>(
value: post,
//判斷是否是當前post
shouldRebuild: (notifier, value) =>
(notifier.id != null && notifier.id == value.id),
builder: (context, value, child) {
return PostItemWidget(
post: value,
click: _skipPostDetail,
);
},
);
// return PostItemWidget2(post: post, click: _skipPostDetail);
}
複製程式碼
最後
所有的示例程式碼都在這 Demo倉庫地址
入口:main_provider.dart -- Selector使用