Riverpodの内部実装を見てみよう①(ref.read編)
Dart/Flutterの状態管理ライブラリとして有名なRiverpodですが、ライブラリの利用から一歩踏み込んで、内部の実装を整理・図解してみます。
- 対象読者: Riverpod / StateNotifierを使ったことがある方
- flutter_riverpod1.0.3、flutter_state_notifier0.7.1を元に解説します
- 全体的な仕組み理解を優先し、コードや図では一部省略し、主要な部分を載せます
(各所にgithubへのリンクを張るので、詳細はそちらからどうぞ!)
おさらい
Riverpodを導入するアプリでは、
- ProviderScopeでアプリ全体を囲み、
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
- 値や状態(state)をラップしたProviderの変数を用意し、
final counterController = StateNotifierProvider((ref) {
return Counter();
});
class Counter extends StateNotifier<int> {
Counter() : super(0);
void increment() => state++;
}
- viewからref(=WidgetRef)を通して状態を取得したり、状態を操作するメソッドを呼ぶことができる
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.read を呼ぶと何が起きるのか?
viewからrefを通して値や状態
(=上記例ではCounterインスタンスや、そのstateであるint)を取得する時、内部では何が起きているかを見ていきましょう。
ここではref.read(counterController.notifier)
(=Counerインスタンスの取得)を見ていきたいと思います。(watchよりシンプルなので)
まず、おさらいの3のbuildメソッドには引数でWidgetRef ref
を受け取っています。
ref ≒ reference(参照)という意味ですね。
おさらいの2の StateNotifierProvider((ref) {
という部分で、providerを定義する際にもrefという名前の引数がありますが、こちらのクラスはStateNotifierProviderRef
でviewの方とは別物になっています。
これについては次回の記事で解説します。
まず、WidgetRefは以下のように定義されています。
abstract class WidgetRef {
T watch<T>(ProviderListenable<T> provider);
void listen<T>(
ProviderListenable<T> provider,
void Function(T? previous, T next) listener, {
void Function(Object error, StackTrace stackTrace)? onError,
});
T read<T>(ProviderBase<T> provider);
State refresh<State>(ProviderBase<State> provider);
}
viewから呼び出すread(やwatch)が生えていますね。
ただ、これはabstractクラスで、3のようにextends ConsumerWidget
したクラスのbuildでは実際にはWidgetRefの実装クラスであるConsumerStatefulElementのインスタンスが来ます。
そのConsumerStatefulElementのreadを見てみると、
T read<T>(ProviderBase<T> provider) {
return ProviderScope.containerOf(this, listen: false).read(provider);
}
ProviderScopeから取得したcontainerに対しreadを呼び出しています。
ProviderScope...これは1で出てきましたね!
ProviderScope.containerOfはwidgetツリーを辿ってProviderContainerを取得するものです。
(ツリーをたどるのはcontainer.dependOnInheritedWidgetOfExactType
等で行っているのですが、この辺の処理についてはInheritedWidget を完全に理解するの記事がおすすめです。)
ProviderContainer...これは名前の通り、counterControllerなどユーザ側で定義したproviderを保持するコンテナで、1でProviderScopeを定義した時に内部的に作られます。
これに対して.read(counterController.notifier)
することでCounterインスタンスが返ってくることになります。
ProviderContainerを深堀りする
ではこのProviderContainerをもう少し詳しく見てみましょう。
まず処理の概要を以下に載せます。
- ProviderScope.containerOfで取得したcontainerに対しreadの呼び出し
- ProviderContainer自身のメソッドを呼び出し
- MapのputIfAbsentメソッドで、provider(key)に対応する_StateReader(value)があればそれを取得。なければ第2引数のFunctionが実行され、_StateReaderをMapに追加
(このMap<...> _stateReadersはProviderContainerのメンバです) - _StateReaderクラスのインスタンスが返る
- readerからProviderElementを取り出す
- elementからStateを読み出して返す
ポイントは _StateReaderクラス と ProviderElementBaseクラス です。
_StateReaderクラス
_StateReaderクラスのコメントには
- ProviderElementBaseオブジェクトを含む
- このオブジェクトはProviderのスコーピング機構を実装するために使われる
とあります。
後者はプロバイダの挙動をオーバーライドする(公式doc)にあるような、テストや一部のviewでproviderを差し替えたい時の機構を指すのですが、本記事では扱いません。
ということで、ここでは
「4の_StateReaderインスタンスに対してgetElementを呼ぶとProviderElementBase(の継承クラス)
のインスタンスが返るんだな」 くらいに捉えればokです。
ProviderElementBaseクラス
ProviderElementBaseクラスは、Providerが保持する値や状態(state)をハンドリングする内部クラスで、今見ているref.read(counterController.notifier)
による取得処理のキモとなる部分です。
6のreadSelf()
の呼び出しで最終的にCounterインスタンスを返します。
ではProviderElementがCounterとどう結びつくのか、getElementに沿って見ていきましょう。
getElementの定義は
ProviderElementBase getElement() => _element ??= _create();
こうなっていて、初回の呼び出し(=_elementがnullの時)に_create()されます。
_createの処理を抜粋すると、
ProviderElementBase _create() {
: // 省略
try {
final element = override.createElement()
.._provider = override
.._origin = origin
.._container = container
..mount();
となっており、overrideに対してcreateElementを呼び出しています。
このoverrideはProviderを指しており、おさらい2ではcounterController.notifier
を指します。
.notifier
というのはStateNotifierProviderのメンバで、以下のようにコンストラクタにて_NotifierProvider
インスタンスがsetされます。
StateNotifierProvider(
// 省略
}) : notifier = _NotifierProvider(
create, // (A)ここにStateNotifierProviderに渡した初期化関数が入る
name: name,
dependencies: dependencies,
from: from,
argument: argument,
),
super(name: name, from: from, argument: argument);
さて、createElement自体は(ElementBaseを継承した)_NotifierProviderElementを返すのですが、これ自体は大した処理は行っていません。
注目すべきは ..mount()
の部分で、ソースから抜粋すると
void mount() {
_mounted = true;
_buildState();
}
void _buildState() {
try {
setState(_provider.create(this));
} catch (err, stack) {
:
_buildState
や setState
とあるように、ここがまさに値や状態(state)を扱う箇所です。
setState(_provider.create(this));
という部分でイメージがわくかと思いますが、
_providerとはcounterController.notifier
(=_NotifierProvider)であり、これのcreateは上記の(A)ここにStateNotifierProviderに渡した初期化関数が入る
の部分、つまり、以下のCounterインスタンスの初期化関数です。
final counterController = StateNotifierProvider((ref) {
return Counter();
});
そしてその実行結果であるCounterインスタンスがsetState
により、
ProviderElementBaseの _state
メンバに保持されます。
ここまでが5のreader.getElement()
で行っている処理です。
最後の6.element.readSelf()
ですが、以下のような3行の処理になります。(ソース)
State readSelf() {
flush();
return requireState;
}
flush()については、今見ているread処理については本筋ではないのでスキップします。
requireStateは以下のような処理で、
Result<State>? getState() => _state;
State get requireState {
final state = getState();
return state.map(
error: (error) => throw ProviderException._(
error.error,
error.stackTrace,
origin,
),
data: (data) => data.state,
);
}
getState()で取得したstate(Result型)に対し、state.mapにより、stateの型がResultError
型であれば(=エラー発生時)throwし、ResultData
型であれば(=正常時)data.stateが返ります。
よって、正常にgetState()できた場合にはCounter
インスタンスが返ります。
まとめ
ここまでで、ざっとref.readの動きを追ってきました。
最後に簡単なクラス図で全体を整理してみます。
(メソッド等はDartの定義=戻り値の型 メソッド名(引数)
で記載します。戻り値の型は一部省略。
(C)は通常クラス、(A)はabstractクラスです。)
以上です。
もし誤り等あれば教えて下さい。
次はref.watchした時の動きを解説します。↓
Discussion