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
が渡されることがわかります。
そのため、selector
でCounterState
の値を扱うことができるわけです。
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
でラップする必要性は薄そう。
参考