Provider adds a selector in the new version, so we can clean up the pit when using a selector

A simple distinction between Consumer and Selector

Earlier versions of the provider had only Consumer listeners, and when notifyListeners() were called in the Model, the Consumer view was rebuilt entirely. After version 3.x, Selector can only select a value in the model to refresh the view in a minimum range, and after version 4.0, we will do a deep check on the value used, and we can also define shouldRebuild.

Two. Common pits

1. If it is the same model, avoid using selector in Consumer

class TestPage extends StatelessWidget {
    @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ChangeNotifierProvider<TestModel>(
        create: (_) => TestModel(),
        child: Consumer<TestModel>(builder: (ctx, testModel, child) {
          debugPrint('Consumer build');
          return Column(
            children: <Widget>[
              Text(testModel.string1),
              RaisedButton(onPressed: () {
                Provider.of<TestModel>(ctx, listen: false).changeString2();
              }, child: Text('change'),),
              Selector<TestModel, String>(
                selector: (_, testModel) => testModel.string2,
                builder: (_, str, c) {
                   debugPrint('string2 build');
                  returnText(str); },)],); }),),); }}Copy the code
  • The selector in the code above is meaningless. When a string2 call changes string2 to trigger a notifyListeners(), the Consumer is rebuilt, and the selector in the wrapper is also updated with rebuild.

The modified

 ChangeNotifierProvider<TestModel>(
        create: (_) => TestModel(),
        child: Selector<TestModel, String>(selector: (_, testModle) => testModle.string1 , builder: (ctx, strint1, child) {
          debugPrint('Consumer build');
          return Column(
            children: <Widget>[
              Text(strint1),
              RaisedButton(onPressed: () {
                Provider.of<TestModel>(ctx, listen: false).changeString2();
              }, child: Text('change'),),
              Selector<TestModel, String>(
                selector: (_, testModel) => testModel.string2,
                builder: (_, str, c) {
                   debugPrint('string2 build');
                  returnText(str); },)],); }),)Copy the code

When string2 changes in model, only the view of string2 will be rebuilt, but string1 will not be rebuilt.

2. An error occurs when using Provider. Of to obtain the value of the model

class TestPage extends StatelessWidget {
    @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: ChangeNotifierProvider<TestModel>(
        create: (_) => TestModel(),
        child: Consumer<TestModel>(builder: (ctx, testModel, child) {
          debugPrint('Consumer build');
          return Column(
            children: <Widget>[
              Text(testModel.string1),
              RaisedButton(onPressed: () {
                Provider.of<TestModel>(context, listen: false).changeString2();
              }, child: Text('change'),),
              Selector<TestModel, String>(
                selector: (_, testModel) => testModel.string2,
                builder: (_, str, c) {
                   debugPrint('string2 build');
                  returnText(str); },)],); }),),); }}Copy the code

The error Could not find the correct Provider above this TestPage Widget. Of (CTX, listen: False).changestring2 (), please refer to the related information about context. Also in Selected, set Listen to false.

3. Dart reference object problem

Dart objects such as List are reference objects, just like JS. So you need to be careful when you use selector to select a reference type object.

ChangeNotifierProvider<TestModel>(
        create: (_) => TestModel(),
        child: Selector<TestModel, List>( selector: (_, testModel) => testModel.numberList, shouldRebuild: (prev, next) => prev.first ! = next.first, builder: (ctx, numberList, child) {return Column(
                children: <Widget>[
                  RaisedButton(
                    onPressed: () {
                      Provider.of<TestModel>(ctx, listen: false).changeNumber(); }, child: Text(numberList.first.toString()), ), Text(numberList.last.toString()) ], ); }),)Copy the code
changeNumber() {
    numberList.first = Random().nextInt(10);
    debugPrint(numberList.first.toString());
    notifyListeners();
  }
Copy the code

In some cases, you might want to select a reference object and rebuild the view only when a value in the reference object changes, as shown in the code above. You might find that numberList.first has changed, but the view does not rebuild. ShouldRebuild will not rebuild even if you cancel the custom and use deep check in the source code.

Look at the Selector source

class _Selector0State<T> extends SingleChildState<Selector0<T>> {
  T value;
  Widget cache;
  Widget oldWidget;

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    final selected = widget.selector(context);

    varshouldInvalidateCache = oldWidget ! = widget || (widget._shouldRebuild ! =null && widget._shouldRebuild.call(value, selected)) ||
        (widget._shouldRebuild == null&&!const DeepCollectionEquality().equals(value, selected));
    if (shouldInvalidateCache) {
      value = selected;
      oldWidget = widget;
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    returncache; }}Copy the code

You can see that selector caches the selected value and stores it in value. If seleted is a reference object, value and selected point to the same reference address. When changeNumber is called, numberList is modified. But the reference address doesn’t change, and the value in the cached value changes as well, so the value and selected are always the same in _shouldRebuild, and the view is not rebuilt.

We do not use deep copy when caching values, but recommend that values be selected immutable

Why selected value must be immutable