Riverpodのソースコードを読む回
Zennに記事を書こうと思ったら、超優秀な前駆者様がいたのでやめました(しかも自分のやつよりめっちゃ詳しいしわかりやすい)
ちゃんと調べてから取り掛かろうね!!!!!!!!!!!!!!!!
/icons/hr.icon
Riverpodは便利だが、内部で何が起きているのかをよく知らないので見に行く
とりあえずリポジトリをクローンしてきたところ
Riverpodのリポジトリは色々入ってるけど、とりあえずRiverpod本体を見る
Riverpodは基本的に
Providerを宣言する
インスタンス変数としてグローバルで保持する
(別にローカルでも問題はない)
宣言したProviderのインスタンス変数を Ref オブジェクトを使って参照する
という流れ。
宣言する部分はまぁ一旦そういうオブジェクトがありますよ、ということで置いておくことにして、まずは
Ref でProviderを参照したら何が起こるのか?
宣言したProviderはどのような形で管理されているのか?
を主軸に見ていく。
小目標として、一番ベーシックな Provider を道標に、いくつかあるProviderの参照方法の中でもよく使う ref.read と ref.watch の実装を見に行くことを考える。
Ref のインターフェース
code:dart
@optionalTypeArgs
abstract class Ref<State extends Object?> {
Provider で使われるのは ProviderRef
code:dart
abstract class ProviderRef<State> implements Ref<State> {
/// Obtains the state currently exposed by this provider.
///
/// Mutating this property will notify the provider listeners.
///
/// Cannot be called while a provider is creating, unless the setter was called first.
///
/// Will throw if the provider threw during creation.
State get state;
set state(State newState);
}
で、これは ProviderElement のインターフェースになってる
code:dart
class ProviderElement<State> extends ProviderElementBase<State>
implements ProviderRef<State> {
ProviderElement は具体すぎる(readとかwatchはいろんなProviderにあってインターフェースがあるはずなので)ので ProviderElementBase をたどってみる
code:dart
/// An internal class that handles the state of a provider.
///
/// Do not use.
abstract class ProviderElementBase<State> implements Ref<State>, Node {
/// Do not use.
ProviderElementBase(this._provider);
ビンゴ〜 ProviderElementBase の中に、ref.read と ref.watch の実装があった やったね!
code:dart
@override
T read<T>(ProviderListenable<T> provider) {
_assertNotOutdated();
assert(!_debugIsRunningSelector, 'Cannot call ref.read inside a selector');
assert(_debugAssertCanDependOn(provider), '');
return _container.read(provider);
}
ちなみに、この ProviderElementBase の中にProviderが保持してるデータ(Riverpod公式的に言うとキャッシュ、ということになるのかな?)が格納されてる
code:dart
/* STATE */
Result<State>? _state;
read の方から追ってみると、ここでは ProviderContainer に処理を委譲してる
code:dart
Result read<Result>(
ProviderListenable<Result> provider,
) {
return provider.read(this);
}
そして ProviderContainer#read では、逆に今まで引き回してきた Provider の方の read を実行し始めた!!
この read は ProviderListenable のものらしいので、とりあえず見てみると
code:dart
@immutable
mixin ProviderListenable<State> {
...
/// Obtains the result of this provider expression without adding listener.
State read(Node node);
そもそもこの人はmixinで、実装は書かれてない :shrugging:
mixinに書かれたインターフェースは実際には具象クラスの方に実装されたものが実行されるので、with ProviderListenable なクラスを探しに行く
code:dart
/// A base class for _all_ providers.
@immutable
abstract class ProviderBase<State> extends ProviderOrFamily
with ProviderListenable<State>
implements ProviderOverride, Refreshable<State> {
全ての Providerの基礎である ProviderBase がそれだった
ProviderBase#read はこんな感じ
code:dart
@override
State read(Node node) {
final element = node.readProviderElement(this);
element.flush();
// In case read was called on a provider that has no listener
element.mayNeedDispose();
return element.requireState;
}
ここで渡されてる Node は追ってきたところでは ProviderContainer で、実態はFlutterだったらちょっと違うかもしれない
今はRiverpod単体の話をしてるので、 ProviderContainer が渡された前提ですすめる。
ProviderContainer#readProviderElement を実行してProviderBase から ProviderElementBase を得て、なんやかんやあって最終的に requireState を通してデータを得る、みたいな流れというのが読み取れる。
一度 ProviderContainer を通すのは、おそらくRiverpodの特徴であるScopedなデータ管理を可能とするため、かな?
readProviderElement を追ってみるが、結構長いので端折るかもしれない
code:dart
@override
ProviderElementBase<State> readProviderElement<State>(
ProviderBase<State> provider,
) {
if (_disposed) {
throw StateError(
'Tried to read a provider from a ProviderContainer that was already disposed',
);
}
final reader = _getStateReader(provider);
// ...
return reader.getElement() as ProviderElementBase<State>;
}
クソ長assertがあったので省略した
ちなみに中身はデバッグモードのときのみ実行するお決まりのやつで、更にassertで Check that this containers doesn't have access to an overridden dependency of the targeted provider なこと(要するにOverrideしたのに元のProviderにアクセスしようとしてないか?)を確認する感じだった
_getStateReader の中身がこれまたクソ長かったので、要約すると
前提として、_StateReader というオブジェクトがあり、この中に対象のProviderを取得できるcontainer(スコープ)、overrideなどの情報を格納できる
Map<ProviderBase, _StateReader>なフィールド _stateReaders から _StateReader を取得できたらそれを直接返す
取得できなかった場合、それは親のcontainerで管理されている、familyである、といったことが考えられるので、それぞれの場合ごとに取得を試みる(この処理が長い、詳細はここでは割愛する)
最終的に得られた _StateReader を _stateReadersにキャッシュしつつ返す
親を探索して得られなかった場合は?と思うかもしれないが、そもそも引数が ProviderBase である以上どこかに宣言されているProviderを探しているのであり、最悪それはRootのcontainerで管理する仕様なので、新しく _StateReader を作ってRootに格納するようになっている
code:dart
// The provider had no existing state and no override, so we're
// mounting it on the root container.
final reader = _StateReader(
origin: provider,
// If a provider did not have an associated StateReader then it is
// guaranteed to not be overridden
override: provider,
container: _root ?? this,
isDynamicallyCreated: true,
);
if (_root != null) {
}
return reader;
とりあえず read に関してはなんとなく見えてきたね やったね!
watch の方をちらっと見てみると、read の方で追った readProviderElement がそのまま出てきてるので watch は read の拡張に当たる、みたいな解釈ができそう
read の方でなんとなく構造はわかってきたので、watch は実際にサンプルコードを用意した上でブレークポイントを貼って追うのが一番早そう
(それを最初からやればいいじゃんと思った方、正解です)
ちょっと長いのでassertを抜いた版を貼っておく
code:dart
@override
T watch<T>(ProviderListenable<T> listenable) {
// ...
if (listenable is! ProviderBase<T>) {
final sub = listen<T>(
listenable,
(prev, value) => _markDependencyChanged(),
onError: (err, stack) => _markDependencyChanged(),
onDependencyMayHaveChanged: _markDependencyMayHaveChanged,
);
return sub.read();
}
// ...
final element = _container.readProviderElement(listenable);
_dependencies.putIfAbsent(element, () {
final previousSub = _previousDependencies?.remove(element);
if (previousSub != null) {
return previousSub;
}
// ...
element
.._onListen()
.._providerDependents.add(this);
return Object();
});
return element.readSelf();
}
サンプルコードはこんな感じ
code:dart
import 'package:riverpod/riverpod.dart';
final container = ProviderContainer();
final base = StateProvider((_) {
return 42;
});
final reader = Provider((ref) {
final state = ref.watch(base);
print('state: $state');
return state;
});
void main(List<String> arguments) {
print('main state: ${container.read(reader)}');
container.read(base.notifier).state = 10;
print('main state: ${container.read(reader)}');
}
とりあえず ProviderElementBase#watch 内のreturnの箇所と、 base の _create 関数内( return 42 の箇所)にブレークポイントを貼ってみた
https://scrapbox.io/files/63958d9354326b001e568d40.png
真ん中あたりの reader.<anonymous closure> あたりから下は reader 内の _create 関数のスコープの話なので、そこから上が ref.watch の実装を追える中身になってそう
https://scrapbox.io/files/6395a2626b2917001efb7b0e.png
( ProviderElementBase#watch の引っかかってる部分、ちょうど記事内では割愛してたassertの部分)あ、そこに引っかかるんだ…って感じだよね
実際中で呼ばれてるのは _container.readProviderElement(listenable) なので、ここから後ろの処理はデバッグモードではバイパスするってことになる
…一通りたどってみたけど、ぶっちゃけ read とやってること変わらんかった :shrug:
ちなみに2回目( StateProvider の state を変更したときの挙動)はというと
https://scrapbox.io/files/6395bb02478ee9001d478a78.png
ふむ?
というか、2回目だと _debugAssertCanDependOn にはいかないんすね
ここでコールスタックを眺めていて、watch をしたときに状態が更新される仕組みが見えてきた
なんやかんやあって ProviderBase#read が実行される
今回の例だと container.read(reader) の部分から、そのままたどった先で実行される
例えばFlutter環境で Provider 以外の場合に watch が起点だった場合でも、実は watch の中で read が実行されている
if (listenable is! ProviderBase<T>) { のブロック
read の中で ProviderElementBase#flush が実行される
flush の中で、_mustRecomputeState フラグが立っている場合は _performBuild が実行される
_performBuild 内では現状の状態を保存しつつ、buildState _notifyListeners の2つを実行する
buildState で _create 関数が実行される(ここが表面上見える動作)
この辺のメソッド名を見てると、少しFlutterのリビルドの仕組みを彷彿とさせる
_notifyListeners がそうだと思う(追ってないから半分推測)けど、依存してるProviderに変更があることを伝えていって(フラグを立てていく)、更新タイミングが来たら更新される的な
更新タイミングは今回の例では2回目に container.read(reader) が実行されたとき
Flutterでは例えば build メソッド実行時とか
というわけで、まとめとしては
Ref でProviderを参照したら何が起こるのか?
基本的に read を介して状態が取得できる
watch の場合は、状態が変更されたら通知、というよりかは、watchで見ている状態が変更されていたら _create を実行し直す、といった感じ
あたかも watch が書いてある _create(Flutterで言えば build) が変更を検知しているように見えるが、そうではないということ
宣言したProviderはどのような形で管理されているのか?
端的に言えば、宣言した Provider そのもの
ただし直接状態変数にアクセスしているわけではなくて、ProviderContainer を介してアクセスすることでスコープを絞る機能を実現している