Flutter狀態管理Provider(四) Selector使用

mwq30123發表於2020-05-01

前言

進入這個帖子,可以先看看前面的介紹哈。

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。

444.gif

程式碼

//首先我們有一個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;
  }
}
複製程式碼

流程圖

通過上面的分析,我們得到一個流程圖

截圖2020-05-01下午12.42.37.png

適用場景

Selector相較於 Cosumer,提供了更細粒度的重新整理控制。
某個元件只關心Model中某個資料變化,可以考慮用Selector,即整體與區域性。
特別需要指明的是selector的結果,必須是不可變的物件。 如果同一個物件,只是改變物件屬性,那shouldRebuild的兩個值永遠是相等的。
我們在 前一篇Provider開發應用 提到的點贊帖子重新整理單個的場景,同樣可以用到Selector

1587347641781-8aac81ad-7f98-4fe4-9918-d2807b939220.gif

我們列出程式碼參考

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使用

相關文章