Riverpodの内部実装を見てみよう②(ref.watch編)
前回はref.readの動きを見ました。
今回はref.watchの動きを見たいと思います。
watchについてはstateが変化するとviewのbuildが再度走ったり(rebuild)、
プロバイダのステートを組み合わせる(公式doc)
final cityProvider = Provider((ref) => 'London');
final weatherProvider = FutureProvider((ref) async {
// `ref.watch` により他のプロバイダの値を取得・監視します。
// 利用するプロバイダ(ここでは cityProvider)を引数として渡します。
final city = ref.watch(cityProvider);
// 最後に `cityProvider` の値をもとに行った計算結果を返します。
return fetchWeather(city: city);
});
のように、他のproviderに依存するproviderがあると適宜再評価されるためreadより複雑です。
本記事ではこれらの 「ref.watchするとどのように変更が検知され、rebuild or 再評価されるのか」 を理解することをゴールとします。
今回読み進めるコード(前回のおさらい)
今回も、前回と同じコードを使いつつ、final count = ref.watch(counterController)
によるrebuildを追いたいと思います。
final counterController = StateNotifierProvider((ref) {
return Counter();
});
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
class MyButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterController); // これを追う
return TextButton(
onPressed: () => ref.read(counterController.notifier).increment(),
child: Text(count.toString()),
);
}
}
初回のref.watchがやっていること
前回、viewでref.read した時、refの実体であるConsumerStatefulElementが
T read<T>(ProviderBase<T> provider) {
return ProviderScope.containerOf(this, listen: false).read(provider);
}
こうなっていることを見ました。
これに対し、watchは
var _dependencies = <ProviderListenable, ProviderSubscription>{};
Res watch<Res>(ProviderListenable<Res> target) {
return _dependencies.putIfAbsent(target, () {
// 省略
return _container.listen<Res>(
target,
(_, __) => markNeedsBuild(),
);
}).read() as Res;
}
こうなっています。
viewからfinal count = ref.watch(counterController)
した時、
return _dependencies.putIfAbsent(target, () {...}).read()
により
_dependenciesというMapのkeyにcounterControllerがあれば対応するvalueを探し、なければputIfAbsentの第2引数の関数を実行してその結果(=_container.listen<Res>(...
)をvalueとしてMapに登録&read()を呼んで返しています。
つまり、readはシンプルにcontainerのreadを呼ぶ(委譲する)のに対し、watchはlistenしてから(=コールバックを登録してから)readするという一手間がかかっています。
では、初回に実行される_container.listen<Res>(...
を見ていきましょう。
...の前に、前回と同じく全体の概要を示します。
1,2は見たので3から見ていきます。
_container.listen
の_container
は前回も出てきたProviderContainerです。
listenの定義は
ProviderSubscription<State> listen<State>(
ProviderListenable<State> provider,
void Function(State? previous, State next) listener, {
// 省略
}) {
// 省略
// 図の4
final element = readProviderElement(provider as ProviderBase<State>);
// 図の5
return element.addListener(
provider,
listener,
// 省略
);
}
こうなっていて、第1引数にcounterController
, 第2引数に(_, __) => markNeedsBuild()
が渡ります。
このmarkNeedsBuildでピンとくる方もいると思いますが、これは次フレームでのwidgetのrebuildを依頼するメソッドです。
listenのコールバック(listener)にこれが指定される == 「Stateが変化したらrebuildしてね」と依頼していることになります。(=listenerの引数がStateのprevious, nextです)
ではこのコールバック(listener)がどう扱われるかをさらに見ていきましょう。
listenerはelement.addListener
に引数として渡されてますね。(図の5)
elementは前回も出てきたProviderElementBaseです。
(ただし、前回はref.readでCounterインスタンスを取得するためのelementだったのに対し、今回はそれが抱えるstateを取得するためのelementという違いがあります。elementの作られ方は後で詳しく見ます。)
ProviderElementBaseは Providerが保持する値や状態(state)をハンドリングする内部クラス なので、いかにもlistenerを受け付けてstateの変化を教えてくれそうです。
addListenerの定義は以下です。
final _listeners = <_ProviderSubscription<State>>[];
ProviderSubscription<State> addListener(
ProviderBase<State> provider,
void Function(State? previous, State next) listener, {
// 省略
}) {
// 省略
final sub = _ProviderSubscription<State>._(
this,
listener,
// 省略
);
_listeners.add(sub); // 図の6
return sub;
}
_ProviderSubscriptionのインスタンスを_listenersに追加してから返すだけです。
class _ProviderSubscription<State> implements ProviderSubscription<State> {
_ProviderSubscription._(
this._listenedElement,
this._listener, {
// 省略
});
final void Function(State? previous, State next) _listener;
final ProviderElementBase<State> _listenedElement;
// 省略
void close() {
// 省略
}
State read() {
// 省略
return _listenedElement.readSelf(); // 図の8
}
}
こうなっており、final sub =
としてインスタンス化した際はシンプルに_listenedElement等のメンバをsetするだけです。
ここまでのコールスタックを逆にたどると、
ProviderElementBaseのaddListenerがsub(=_ProviderSubscription)を返す(図の7)
→ ProviderContainerのlistenがsubを返す(図の8)
→ ref.watch内の_dependencies.putIfAbsent(...
がsubを返す
となります。
ref.watchはputIfAbsentの結果に対してread()
の結果をreturnするので、
ref.watchは_ProviderSubscriptionのread()結果を返すということですね!
_ProviderSubscriptionのreadとは_listenedElement.readSelf()
(図の8)、つまりProviderElementBase
のreadSelf()でこれは前回解説しました。
よって、ここまでをまとめると
初回のref.watchは、watch対象のオブジェクトをハンドリングするelement(ProviderElementBase)にコールバックを登録した上で、ref.readと同じくelementから値を読み出して返す
といえます。
stateが変化した時に起きること
次に、stateが変化した時の動きを見ていきましょう。
今回の例ではMyButtonをタップしてref.read(counterController.notifier).increment()
した後にどうrebuildが走るのかを追っていくことになります。
ただし、前節で_container.listenのlistenerコールバックにmarkNeedsBuild()
が指定されていることは見たので、どういうフローでProviderElementBaseに登録したlistenerコールバックが呼ばれるのか を見ていくことになります。
ref.read(counterController.notifier)でCounterインスタンスを取得するフローは前回見ました。
increment()は
void increment() => state++;
こういう定義で、state = state + 1
と同義です。
StateNotifierのstateにはsetterが生えており、
final _listeners = LinkedList<_ListenerEntry<T>>();
T _state;
set state(T value) {
// 省略
_state = value;
final errors = <Object>[];
for (final listenerEntry in _listeners) {
try {
listenerEntry.listener(value); // listenerを呼んでる!
} catch (error, stackTrace) {
errors.add(error);
// 省略
}
}
// 省略
}
_listenersをループしてlistenerコールバックを呼んでいます。
これは前節で見たlistenerと関係あるのでしょうか・・?
(でもこの_listenersはStateNotifierのメンバであり、ProviderElementBaseではありません)
ということで、StateNotifierの_listenersがどう作られるかを見ていきましょう。
引き続きStateNotifierを見ていくと、以下のようにaddListenerメソッドで_listenersにエントリを追加していることがわかります。
RemoveListener addListener(
Listener<T> listener, {
bool fireImmediately = true,
}) {
// 省略
final listenerEntry = _ListenerEntry(listener);
_listeners.add(listenerEntry); // ここで追加してる
このaddListenerはどこで呼ばれるのでしょうか?
実は 初回のref.watchで(ProviderElementBaseによってハンドリングされる)state(int値)が作られる時(前節の図の4) に呼ばれています。
このstateが作られる動きをもう一度丁寧に見ていきましょう。
ProviderElementBaseはreadProviderElement(前節図4)の中で、
_StateReaderのgetElement()を呼ぶことで作られますが、
ProviderElementBase getElement() => _element ??= _create();
ということで初回だけ_create()され、その中身は
ProviderElementBase _create() {
: // 省略
try {
final element = override.createElement()
.._provider = override
.._origin = origin
.._container = container
..mount();
こうなっているのでした。(必要に応じて前回の記事を参照して下さい)
ここのoverrideはStateNotifierProvider
を指します。
(viewからのref.watchの引数が counterController == StateNotifierProvider なので)
その後のelementに対するmount()の中で
setState(_provider.create(this));
が実行されます。(_providerはStateNotifierProvider
、thisはelement)
ではStateNotifierProvider
のcreateを見てみましょう。
(※前回見たref.read(counterController .notifier)では、StateNotifierProviderに引数として渡した関数がcreateで呼ばれるのに対し、今回(=ref.watch(counterController))はStateNotifierProvider自体のcreateが呼ばれるという違いに注意して下さい。)
State create(ProviderElementBase<State> ref) {
final notifier = ref.watch(this.notifier);
void listener(State newState) {
ref.setState(newState);
}
final removeListener = notifier.addListener(listener); // 出たーー
ref.onDispose(removeListener);
return ref.requireState;
}
出やがったな... addListenerが...!(ざわっ...)
このaddListenerはStateNotifierに対する呼び出しなのか一応確認しましょう。
addListenerが呼ばれるnotifierはref.watch(this.notifier)
で取得されます。
このref.watch(=ProviderElementBaseのwatch)は何をしているのでしょうか?
(※viewが呼ぶWidgetRefのwatchとは別物であることに注意して下さい。)
T watch<T>(ProviderListenable<T> listenable) {
// 省略
final provider = listenable as ProviderBase<T>;
final element = _container.readProviderElement(provider);
_dependencies.putIfAbsent(element, () {
// 省略
element._dependents.add(this);
return Object();
});
return element.readSelf();
}
watchの引数には this.notifier
== counterController .notifier が指定されるので、
_container.readProviderelement(provider)
ではCounterインスタンスを保持するProviderElementBaseが返ります。
(要は前回見たviewからのref.read(counterController.notifier)でも登場したやつですね。
ということは、、viewからの初回ref.watch(counterController)の過程でviewからのref.read(counterController.notifier)相当の処理が走り、Counterインスタンスとそれが抱えるstateが作られるということです)
このelementの _dependents
にthisを追加するというのは、
Counter(StateNotifier)を保持するelement(依存元)に対し、Counterのstate(int値)のelementが依存してるよ(依存先) というのを記録しているわけですね。...(A)
(_dependentsについては、後でproviderの再評価の方を見るときに出てきます)
そしてreadeSelf()でCounter(=StateNotifier)インスタンスを返しています。
ということで、
final notifier = ref.watch(this.notifier)
のnotifierとはCounterインスタンスであり、
final removeListener = notifier.addListener(listener)
で
StateNotifierのaddListenerが呼ばれていることが確認できました。
そして、ここでaddしたlistenerは
void listener(State newState) {
ref.setState(newState);
}
です。
listener内のrefはstate(int値)のProviderElementBaseであり、そのsetStateを呼んでいます。
前回の記事ではsetStateは _stateメンバに値をsetする
程度しか触れていなかったですが、
実は
void setState(State newState) {
// 省略
final result = _state = Result.data(newState);
if (_didBuild) {
_notifyListeners(result, previousState);
}
}
_stateにsetした後に_notifyListenersにより依存先への通知 == listenerの呼び出し を行っています。
そのlistenerこそ、最初の方の_container.listen
で指定された
(_, __) => markNeedsBuild()
です!
長くなってきたので_notifyListenersの中身は折りたたみで載せます
void _notifyListeners(
Result<State> newState,
Result<State>? previousStateResult,
) {
// 省略
final previousState = previousStateResult?.stateOrNull;
// 省略
final listeners = _listeners.toList(growable: false);
final subscribers = _subscribers.toList(growable: false);
newState.map(
data: (newState) {
for (var i = 0; i < listeners.length; i++) {
Zone.current.runBinaryGuarded(
// ここで呼んでる!!
listeners[i]._listener,
previousState,
newState.state,
);
}
for (var i = 0; i < subscribers.length; i++) {
Zone.current.runBinaryGuarded(
subscribers[i].listener,
previousState,
newState.state,
);
}
},
error: (newState) {
// 省略
},
);
for (var i = 0; i < _dependents.length; i++) {
_dependents[i]._didChangeDependency();
}
// 省略
}
こうなっており、listeners(=_ProviderSubscriptionの配列)をループし、
各要素の_listener(=markNeedsBuild()の入ったコールバック)を呼び出しています。
これでviewからのref.watchによるlistenerの登録と、
increment()によるlistenerの呼び出しがつながりました。
なかなか複雑ですね。。
listenerの登録周りを図にすると以下のようになります。
(番号を順番に追ってみて下さい)
全体を簡単にまとめると、
- 初回ref.watchでProviderElementBaseにmarkNeedsBuildのコールバック...①が登録される
- そのProviderElementBaseを作る際に、StateNotifierのコールバックとして①の実行を含む処理を登録する
- increment()するとStateNotifierのコールバックを通して①を実行してrebuildする
ということになります。
viewのrebuildはわかった。他providerに依存するproviderの再評価はどうなる?
見ていきましょう。
counterの例を使うと
final counterController = StateNotifierProvider((ref) {
final bar = ref.watch(barController); // 他providerに依存
return Counter();
});
こんな感じですね。
viewからref.read(counterController.notifier)
した場合、
StateNotifierProviderに渡す初期化関数は、
void mount() {
_mounted = true;
_buildState();
}
void _buildState() {
try {
setState(_provider.create(this)); // ここ
} catch (err, stack) {
:
で実行されるのでした。
この時、createの引数のthis(=初期化関数の引数のref)はProviderElementBaseを継承した_NotifierProviderElementです。
つまり、final bar = ref.watch(barController);
は、
_NotifierProviderElementのwatchを呼び出しているということです。
このwatchは継承元のProviderElementBaseで定義されており、、
前節のfinal notifier = ref.watch(this.notifier);
で登場しました!(再掲)
T watch<T>(ProviderListenable<T> listenable) {
// 省略
final provider = listenable as ProviderBase<T>;
final element = _container.readProviderElement(provider);
_dependencies.putIfAbsent(element, () {
// 省略
element._dependents.add(this);
return Object();
});
return element.readSelf();
}
ここで(A)を今回の呼び出しに当てはめると、
barControllerを保持するelement(依存元)に対し、counterControllerのelementが依存してるよ(依存先)
と登録していることになります。
そして、前節でincrement()でstateを更新した時、ProviderElementBaseのsetStateで_notifyListeners
が呼ばれることを説明しましたが、
実はこの_notifyListenersの中で
for (var i = 0; i < _dependents.length; i++) {
_dependents[i]._didChangeDependency();
}
という処理が行われています。(折りたたみ箇所をご覧ください)
つまり、barControllerのstateが更新された時、counterControllerのelementの_didChangeDependency()
が呼ばれるということです。
後はmarkMustRecomputeState()が呼ばれ、stateの再計算ということでもう一度StateNotifierProviderに渡した初期化関数が呼ばれるという流れになります。
おまけ
前回の記事で
おさらいの2の StateNotifierProvider((ref) { という部分で、providerを定義する際にもrefという名前の引数がありますが、こちらのクラスはStateNotifierProviderRefでviewの方とは別物になっています。
と書きましたが、viewのrefはWidgetRef(ConsumerStatefulElement)であったのに対し、
StateNotifierProviderに渡す初期化関数のrefは、StateNotifierProviderRefという同様のinterface(=watch/read/listen etc)を備えたProviderElementBaseだった
ということですね。
おわりに
ここまでStateNotifierProviderを例に、ref.read/watch の動きを見てきました。
本記事の内容を把握すれば、あとは他のProvider(ChangeNotifierProfider等)やautoDispose, familyあたりも読み進めやすいんじゃないかなと思います。
そしてふと気づけば、もうすぐriverpod2.0が出るようですね。(2022/3/20現在)
色々動きが早くて大変ですが、がんばって追いかけるぞ...!(泣)
Discussion