🛫

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

2022/03/20に公開

前回はref.readの動きを見ました。
https://zenn.dev/junq/articles/fa3dfd24a7ab84

今回は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に追加してから返すだけです。

_ProviderSubscription

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)

ではStateNotifierProvidercreateを見てみましょう。
(※前回見た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の中身は折りたたみで載せます

_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