🤩

Riverpodの内部実装を見てみよう①(ref.read編)

2022/03/20に公開約9,600字

Dart/Flutterの状態管理ライブラリとして有名なRiverpodですが、ライブラリの利用から一歩踏み込んで、内部の実装を整理・図解してみます。

  • 対象読者: Riverpod / StateNotifierを使ったことがある方
  • flutter_riverpod1.0.3、flutter_state_notifier0.7.1を元に解説します
  • 全体的な仕組み理解を優先し、コードや図では一部省略し、主要な部分を載せます
    (各所にgithubへのリンクを張るので、詳細はそちらからどうぞ!)

おさらい

Riverpodを導入するアプリでは、

  1. ProviderScopeでアプリ全体を囲み、
void main() {
 runApp(
   ProviderScope(
     child: MyApp(),
   ),
 );
}
  1. 値や状態(state)をラップしたProviderの変数を用意し、
final counterController = StateNotifierProvider((ref) {
  return Counter();
});

class Counter extends StateNotifier<int> {
  Counter() : super(0);

  void increment() => state++;
}
  1. 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は以下のように定義されています。

consumer.dart
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をもう少し詳しく見てみましょう。

まず処理の概要を以下に載せます。

  1. ProviderScope.containerOfで取得したcontainerに対しreadの呼び出し
  2. ProviderContainer自身のメソッドを呼び出し
  3. MapのputIfAbsentメソッドで、provider(key)に対応する_StateReader(value)があればそれを取得。なければ第2引数のFunctionが実行され、_StateReaderをMapに追加
    (このMap<...> _stateReadersはProviderContainerのメンバです)
  4. _StateReaderクラスのインスタンスが返る
  5. readerからProviderElementを取り出す
  6. 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行の処理になります。(ソース)

provider_base.dart
  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した時の動きを解説します。↓

https://zenn.dev/junq/articles/73a9db44393f4a

Discussion

ログインするとコメントできます