5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ProviderのSelectorを読み解く

Last updated at Posted at 2020-03-07

Selectorとは

Flutterでは現在、ChangeNotifierを継承したクラスで状態を管理し、Provider.ofで下位ウィジットでピンポイントに値を取得するというのが状態管理のメジャーな手段かと思われます。

これを用いることで、notifyListeners()を呼び出すことで手軽に状態を変更することができますが、
気をつけないとProvider.of(context, listen: true)でプロパティを取得しているところ全てでリビルドが発生してしまい、多少無駄な処理が発生することになります。

こんな時はSelectorを使用することで、指定したプロパティを監視し、値が変化した時のみビルドを発火させることが可能です。
さらに、shouldRebuildを指定することで、監視対象のプロパティが特定の値の時だけビルドを発火させるということも可能です。
では、どのようにしてプロパティを監視しているのか解明するため、Selectorの実装を見ていきます。

サンプルコード

ChangeNotifierで2つのカウンターを管理し、counterAに反応するウィジットを考えます。

class CounterViewer extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Selector<CounterState, int>(
      selector: (context, state) => state.counterA, // 監視対象のプロパティを返却
      builder: (context, counter, child) {  // selectorで指定したプロパティが変更されるたびにビルドされる。
        return Text('current count: $counter');
      },
    );
  }
}

class CounterState extends ChangeNotifier {
  int counterA = 0;
  int counterB = 0;
  void incrementA() {
    counterA++;
    notifyListeners();
  }

  void incrementB() {
    counterA++;
    notifyListeners();
  }
}

Selectorの実装は以下の通りです。

class Selector<A, S> extends Selector0<S> {
  /// {@macro provider.selector}
  Selector({
    Key key,
    @required ValueWidgetBuilder<S> builder,
    @required S Function(BuildContext, A) selector,
    ShouldRebuild<S> shouldRebuild,
    Widget child,
  })  : assert(selector != null),
        super(
          key: key,
          shouldRebuild: shouldRebuild,
          builder: builder,
          selector: (context) => selector(context, Provider.of(context)),
          child: child,
        );
}

Selectorは主に基底クラスのSelector0のコンストラクタを呼び出しているだけですが、selectorに注目です。
selectorの第二引数には、Provider.ofで取得したInheritedWidgetが渡されることがわかります。
そのため、selectorCounterStateの値を扱うことができるわけです。

Selector0の実装は以下の通りです。

class Selector0<T> extends SingleChildStatefulWidget {
  /// Both `builder` and `selector` must not be `null`.
  Selector0({
    Key key,
    @required this.builder,
    @required this.selector,
    ShouldRebuild<T> shouldRebuild,
    Widget child,
  })  : assert(builder != null),
        assert(selector != null),
        _shouldRebuild = shouldRebuild,
        super(key: key, child: child);

  final ValueWidgetBuilder<T> builder;
  final T Function(BuildContext) selector;
  final ShouldRebuild<T> _shouldRebuild;
  @override
  _Selector0State<T> createState() => _Selector0State<T>();
}

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

  @override
  Widget buildWithChild(BuildContext context, Widget child) {
    // selectorに渡したメソッドから、監視対象プロパティ(counterA)を取得する。
    final selected = widget.selector(context); 

    // キャッシュしない条件を計算する
    //  - 初回呼び出し時
    //  - shouldRebuildがtrueの時
    //  - selectorで算出したプロパティが変化した時
    var shouldInvalidateCache = oldWidget != widget ||
        (widget._shouldRebuild != null &&
            widget._shouldRebuild.call(value, selected)) ||
        (widget._shouldRebuild == null &&
            !const DeepCollectionEquality().equals(value, selected));
    if (shouldInvalidateCache) {
      // 値をstateにキャッシュする
      value = selected;
      oldWidget = widget;
      // Selectorに渡したbuilderはここで実行される。
      cache = widget.builder(
        context,
        selected,
        child,
      );
    }
    return cache;
  }
}

継承元のクラスを見る通り、Selectorは要するに、子を1つ持つStatefulWidgetです。(実際Selectorにはchildを指定できる)
変化前の値とbuilderをstateに保存しておき、場合に応じてキャッシュしたbuilderを返却しています。

結局はProvider.ofは常に発火するため、Selectorはリビルドされています。
Selectorは、プロパティの変化に応じてbuilderで子を新しく作り直すか、キャッシュした子をそのまま返すかを行っているだけです。

まとめ

  • Selectorの正体は実質StatefulWidgetである。
  • 結局はProvider.ofは常に発火するため、Selectorはリビルドされる。
  • Provider.ofによる値の変化値をSelectorでキャッシュしておき、プロパティの変化に応じて新たにbuilderで子を作成し返却もしくは、キャッシュされた子を返却する。
  • 結局Selectorがリビルドされるので、かなり大きいウィジットをbuilderで返さない限り、わざわざSelectorでラップする必要性は薄そう。

参考

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?