EMoshU Blog
2024/11/12
2024-11-13
Satoshi

Riverpodの概要と内部処理について

Flutterの状態管理パッケージとして、現在スタンダードとなっているRiverpodについて学習したので、説明しました。

これからRiverpodを学習される方の一助となれば幸いです。


 

こんにちは、さとしです。

前回は、状態管理ライブラリの一つである Provider を取り上げ、概要と内部処理について解説していきました。

 

今回は、同じく状態管理パッケージである Riverpod の概要と、Riverpod を利用する際に使われることの多い read, watch メソッドの内部処理の説明を行います。

 

Flutter 初学者の方で、 Provider, InheritedWidget のブログをまだご覧になっていない方は先にそちらをご覧ください。

 

なお、今回のブログ執筆にあたり利用したパッケージは以下の通りになります。

 

flutter_riverpod: 2.5.1

flutter_lints: 3.0.0

riverpod_generator: 2.4.0

build_runner: 2.4.9

custom_lint: 0.6.4

riverpod_lint: 2.3.10

 

また、RiverpodGenerator を利用しています。予めご承知ください。

RiverpodGenerator は、Riverpodを利用して実際に状態管理を行うための Provider の定義を実装しやすくするためのライブラリです。

このブログでは詳細な説明をしませんが、以下のサイトに詳しい説明がありますので、参考にしてください

 

【Flutter】Riverpod Generator基本

 

 

 

Riverpod の概要

Riverpod は、Provider と同じ開発者によって実装された状態管理パッケージです。

 

Provider のメジャーアップデートとして公開される予定のものだったということもあり、Provider の上位互換といって良いものとなっています。

 

Provider と比較して Riverpod が優れている点としては、Provider の範囲外からアクセスの場合の挙動が挙げられます。

Provider の場合は、アクセスエラーはランタイムエラーとして発生します。

つまり、ネイティブアプリの場合はクラッシュが発生するまでエラーを検知することができません。

 

しかし、Riverpod ではコンパイルエラーとして発生するため、範囲外アクセスが発生した場合そもそもビルドを行うことができないので、より安全に利用することができます。

 

Riverpodの内部処理

Riverpod では、状態を保持しているProvider にアクセスしたい Widget は ConsumerWidget を継承する必要があります。

 

ConsumerWidget というのはRiverpod独自のWidget であり、Widgetツリーの親子関係を保持するための BuildContext と、 Provider にアクセスして状態を取得するためのWidgetRef を持っています。

 

Provider では、context.watch など BuildContext を利用して 状態を保持する Provider にアクセスしますが、Riverpod では WidgetRef が持つメソッドである watch、readメソッド などを呼び出してProviderにアクセスします。

 

ConsumerWidgetには、WidgetRef を通じて Provider からデータを取得するためのメソッドとして、read メソッド、watch メソッド、listen メソッドの3つが用意されています。

 

read メソッドは、Provider が保持しているデータを読み取るためのメソッド

watch メソッドは、Provider が保持しているデータの読み取り + Provider が保持しているデータに更新があった場合は、watch メソッドを呼び出している ConsumerWidget を再描画します。

listen メソッドは、データの読み取りと、Provider が保持しているデータに更新があった場合、引数に渡した処理の実行となります。

 

・readメソッドの内部処理

 

サンプルアプリを用いて、図を用いながら、read メソッドの内部処理を解説したいと思います。

 

サンプルアプリは、以前と同様に Flutter プロジェクトを作成した際のデフォルトアプリであるカウンターアプリを用います。

ソースコードは下記を参照いただければと思います。

https://github.com/EMoshU/riverpod-counter-app-blog/blob/main/lib/main.dart

 

このアプリの Widget ツリーは下記の図のような構造になっています。

図1_riv本番.png

図1.サンプルアプリのWidgetツリー

 

read メソッドは、引数に渡した Provider から値を読み取るメソッドです。

 

read メソッドは ConsumerWidget と対になり、WidgetRef を継承している ConsumerStatefulElement クラスのメソッドであり、内部でProviderScope.containerOf メソッドを呼び出しています。

 

ProviderScope.containerOf メソッドは、呼び出した ConsumerStatefulElement から直近の Element ツリーに存在する ProviderScope が保持しているProviderContainer を取得するメソッドです。

 

実際に内部の処理を見てみると、前回紹介した Provider のブログ でも登場した  1-1.getElementForInheritedWidgetOfExactType メソッドを呼び出しています。

 

ここでは、InheritedWidget を継承した UnControlledProviderScope を返しています。

 

その後、取得した UnControlledProviderScope が持っている ProviderContainer を返しています。

 

続いて、データを持っている ProviderContainer から、実際に値を取り出す処理について、図2を元に説明していきます。

 

ProviderScope.containerOf メソッドで取り出された ProviderContainer の2-1.read メソッドを呼び出します。

 

ProviderContainer の read メソッド内では、引数として渡された Provider の 2-2.read メソッドを呼び出して、戻ってきた Result 型の値を呼び出し元に返すだけです。

 

Provider の read メソッドは、 ProviderListenable を mixIn している ProviderBase クラスに存在していて、内部で 2-3.readProviderElement メソッド を呼び出します。

readProviderElement メソッド内では、新しく Provider を生成して、初期値を設定した上で ProviderElementBase を返します。

 

2-5 flush メソッドは、取得した ProviderElementBase に対して、リビルドを行うか判定する処理です。

 

2-7 mayNeedDispose メソッド は、取得した ProviderElementBase に対して 破棄の予約をするかどうかを判定する処理です。

ConsumerWidget が持つ read メソッド(1-1の処理)は、keepAlive という引数を持つことができます。それを設定しない場合には ProviderElementBase の破棄予約がされることになります。

 

その後、2-9.requireState メソッドで ProviderElementBase から値を取得し、それを ConsumerStateElement まで返すことになります。

図2_riv本番.png

図2.read メソッドの処理のシーケンス図

 

・watchメソッドの内部処理

 

次に、watch メソッドについて説明します。

このメソッドは、Provider で保持している値に変更があった場合、Widget の再描画を行うものになります。

 

まずは、初期表示時に値を取得する処理を図を用いて説明していきます。

 

ConsumerStatefulElement の watch メソッドが呼ばれると、3-1._putIfAbsent メソッドが呼ばれます。

 

 

これは、前に同じProvider が作成されていないかを判定する処理です。もし既に同じ Provider が作成されている場合は以降の処理は全てスキップされますが、初期表示の場合は初めて作成することになるので、後続処理が行われます。

 

後続処理として、ProviderContainer.listen メソッドが呼び出されます。このメソッドは Provider と関数の2つの引数を取り、1つめの引数に渡したProvider に更新があった場合、2つめの引数に渡した関数を起動する仕組みになっています。

 

ここでは、markNeedsBuild を設定しており、この仕組みによって watch メソッドはデータの更新に応じて画面の再描画を行うことを可能としています。

 

 

次に、3-3.read メソッドを呼び出し、ProviderSubscription から値を取得します。

 

read メソッドの内部では、3-4.readSelf メソッドが呼び出されており、内部では、先ほどの ref.read メソッドでも登場した ProviderElementBase.flush メソッドが呼ばれ、リビルドの有無を判定しています。

 

また、ProviderElementBase から 3-6.requireState メソッドを呼び出し、 値を取得して返しています。

こちらは、2-9 の処理と同じものになります。

図3_riv本番.png

図3.watch メソッドで初期表示時にデータを取得するまで

 

次は、FloatingActionButton が押下されてから、インクリメントされた数値がセットされ、再描画されるフローを説明していきます。

 

FloatingActionButton を押下すると、Counter クラスの increment メソッドが呼ばれ、CounterProvider が保持している state のインクリメント処理が走ります。

 

内部では、インクリメントされた後の新しい値を引数に渡した上で、4-2.ProviderElementBase.setState メソッドが呼ばれます。

 

ProviderElementBase.setState メソッド内では、getState メソッドを呼び出してインクリメント前の値を取得し、新しい値と古い値をセットして 4-3._notifyListeners メソッドを呼び出します。

_notifyListeners メソッド内部で、図4で説明した ProviderContainer.listen で設定した Listener を呼び出し、リビルド処理を行います。

 

ConsumerWidget の再描画が終わったら、4-4._markDependencyChanged メソッドが呼び出されます。この処理は、更新されたデータに依存しているConsumerWidget(ConsumerStateElement)に対して既にデータが更新されたことをProviderにマークします。

 

4-5.invalidateSelf, そしてinvalidateSelf メソッドから呼び出される 4-6.runOnDispose メソッドは、一連の更新通知が完了した後に呼び出され、自身に対する購読処理を無効化する処理になります。

4-7.mayNeedDispose は、ref.read メソッドの解説の際にも出てきた、自身を破棄するか判定するメソッドです。

最後に、4-8.ProviderScheduler.scheduleProviderRefresh メソッドを呼び出し、新しい状態を持った Provider の再生成の予約を行います。

 

図4_riv本番.png

図4.watch メソッドで FloatingActionButton が押下されてから リビルドされるまで

 

 

まとめ

ここまで、Riverpod の特徴と、その内部処理について解説していきました。

 

Riverpod は Provider が持つ課題を解消したパッケージです。

範囲外アクセスがコンパイル時エラーとして検知できるようになったことで、開発時の安全性も高まりました。

更に、RiverpodGenerator などの自動生成パッケージなどの追加により更に使いやすくなった Riverpod は、現在の状態管理パッケージのスタンダードとなっています。

 

ここまで Flutter 開発における状態管理の方法として、 InheritedWidget, Provider, Riverpod の3つを紹介してきました。

今回で、Flutter 開発における状態管理の方法の紹介ブログは以上となります。

 

今後も、 Flutter の学習の中で得た知見をブログで紹介できたらと思います!

 

さいごに

 

EMoshU アプリチームでは、自社アプリで Flutter を採用すべく Flutter エンジニアを募集しております。

 

・今の会社・環境では、現場が変わると人間関係がリセットされてしまい、新しく人間関係を築き続けるのに疲れる。寂しい

・一緒にワイワイ開発出来る仲間が欲しい!

・チームを強くする経験を積みたい!

・自社プロダクトでガンガン Flutter を使っていきたい方

 

という方、ぜひ募集要項をご覧ください。カジュアル面談なども実施しております。

Flutter を使ったアプリ開発で世の中に貢献したい方、ぜひご応募ください!!

 

募集要項