provider之selector

時崎狂三_發表於2020-02-25

provider在新版本中新增了selector,整理一下selector使用時候會踩的坑

一.Consumer和Selector的簡單區別

provider早期版本中只有Consumer,當model中呼叫notifyListeners()的時候,對應的Consumer的檢視就會整個rebuild,而在3.x版本之後出現Selector可以僅選擇model中某個值來最小範圍的重新整理檢視,並且在4.0版本之後會對使用的值進行deep check,也可以自己自定義shouldRebuild。

二. 常見坑

1.如果為同一個model,避免在Consumer中使用selector

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');
                  return Text(str);
                },
              )
            ],
          );
        }),
      ),
    );
  }
}

複製程式碼
  • 上面程式碼中selector是沒有意義的,當呼叫changeString2修改string2觸發notifyListeners()的時候, Consumer就會重新rebuild,包裹下的selector也會更新rebuild。

修改後

 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');
                  return Text(str);
                },
              )
            ],
          );
        }),
      )
複製程式碼

如上修改後,當model中string2變化的時候,就只會rebuild string2的檢視,string1並不會rebuild。

2.使用Provider.of獲取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');
                  return Text(str);
                },
              )
            ],
          );
        }),
      ),
    );
  }
}
複製程式碼

如上程式碼是會報 Could not find the correct Provider above this TestPage Widget的錯誤,修改為 Provider.of(ctx, listen: false).changeString2(), 關於context問題請查閱相關資料,這裡不再贅述。同時在selected中注意設定listen為false。

3.dart中的引用物件問題

dart中List等屬於引用型別物件,和js一樣。由此當使用selector選擇的值為引用型別物件的時候,需要特別注意。

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())
                ],
              );
            }),
      )
複製程式碼
changeNumber() {
    numberList.first = Random().nextInt(10);
    debugPrint(numberList.first.toString());
    notifyListeners();
  }
複製程式碼

在某種場景下,可能有時候會和如上程式碼所示一樣希望selected一個引用物件,僅希望在引用物件中某個值變化的時候rebuild檢視,然而會發現通過上面的程式碼會發現numberList.first已經改變,但是檢視不會rebuild。就算是取消自定義的shouldRebuild使用原始碼中的deep check也依然不會rebuild。

看下Selector原始碼

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);

    var shouldInvalidateCache = 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,
      );
    }
    return cache;
  }
}
複製程式碼

可以發現selector會快取selected的值並儲存到value,如果seleted的值為引用物件的時候,value和selected指向同一個引用地址,當呼叫changeNumber的時候,修改了numberList的值,但是引用地址沒變,快取的value中的值也隨著發生變化,所以在_shouldRebuild的時候對比的value和selected一直是一樣的,檢視也就不會rebuild。

官方在快取value的時候並沒有使用深拷貝,而是推薦選擇的值為immutable

Why selected value must be immutable

相關文章