Flutter状态管理Provider(四) Selector使用

前言

进入这个帖子,能够先看看前面的介绍哈。git

Selector

介绍

selector.dart中有段关于Selector的介绍github

/// {@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 方法供外部类去修改。markdown

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

效果

页面上有三个字母按钮,统计每一个按钮点击次数。点击按钮,则按钮上数字+1。 less

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内部的实现。ide

当更新S时,比较先后S值是否相同。 不相同,则从新构建而且缓存住。相同,则使用cache的Widgetoop

解读

//省略了部分代码
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
post

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