【Flutter】Riverpod 入門
RiverpodはFlutterの状態管理パッケージです。Riverpodが登場する以前は、providerパッケージがメジャーなパッケージとして多くのプロジェクトで使用されてきました。
providerパッケージは非常に強力なパッケージである一方、同じ型のProviderを同時に利用することができない、スコープ外からProviderにアクセスするとProviderNotFoundException エラーが発生するといった弱点があります。
Riverpodはそのproviderパッケージの弱点を改良した上位互換のパッケージとなります。
Riverpodパッケージの種類
Riverpod関連のパッケージは3種類あります。
| パッケージ名 | 採用基準 | アプリの形態 |
|---|---|---|
| flutter_riverpod | Flutterを使用している | Flutterのみ |
| hooks_riverpod | 既にflutter_hooksパッケージを使用している | Flutter + flutter_hooks |
| riverpod | Flutterを使用していない | Dart のみ |
flutter_riverpod とhooks_riverpod は、Flutterを使用してアプリを開発する時に使用します。既にflutter_hooks をプロジェクトに導入している場合や、Hooks機能を使用する時はhooks_riverpod を使用し、それ以外の時はflutter_riverpod パッケージを使用します。riverpod はFlutterを使用せずにアプリを開発する時に使用します。この記事ではflutter_riverpod を使用して説明を進めていきます。
flutter_riverpodパッケージのインストール
flutter_riverpod パッケージをインストールするため、pubspec.yaml のdependencies に以下のコードを記述します。
そしてflutter pub get コマンドを実行することでRiverpodがプロジェクトに追加されます。
Riverpodの基本的な使い方
ルートにProviderScopeを追加する
ProviderScope で上位ツリーのWidgetを囲むと、下位ツリーのWidgetでProviderを呼び出すことができるようになります。以下のコードではMyApp() をProviderScope で囲むことでMyApp以降のWidgetでProviderを呼び出すことができるようにしています。
Providerをグローバル変数として定義する
Providerとはデータを管理する入れ物のようなクラスです。Riverpodでは様々な特徴を持つProviderが用意されています。今回は最も基本的なProvider を使用します。
Provider の引数に指定したコールバック関数でreturn するものが管理するデータになります。コールバック関数の引数にProviderRef 型の引数ref を指定します。これは他のProviderのデータを取得したい時に使用します。上記コードでは他のProviderのデータを取得ししていないので、ref ではなく_ としても問題ありません。
Providerからデータを取得する
Providerからデータを取得するためにConsumerWidget を使用します。ConsumerWidget を継承したWidgetを定義すると、build() にWidgetRef 型の引数が追加されます。
WidgetRef 型引数のwatch() またはread() の引数にProviderを指定することでProviderが管理しているデータを取得することができます。上記コードでは、ProviderにstrProvider を指定しているので、strProvider が管理しているデータである'Hello Riverpod' を取得することができ、変数valueに代入しています。データの変更を監視する必要がある時はwatch() を、監視する必要がない時はread() を使用してデータを取得します。
全体コード

ここまでがRiverpodの基本的な使い方になります。
次項以降では各種Providerの説明やProvider修飾子といった、Riverpodを使いこなすために更に踏み込んだ説明を行います。最初から全てを覚えようとする必要はなく、Riverpodの基本的な使い方をインプットしていただいた後に、必要に応じて参考にしていただく程度で良いと思います。
Providerの種類
Riverpodにはさまざまな種類のProviderが用意されています。各Providerの特徴を把握して適切なProviderを選択するようにしましょう。
| Provider名 | 管理するデータの型 |
|---|---|
| Provider | 任意 |
| StateNotifierProvider | StateNotifier のサブクラス |
| FutureProvider | 任意の Future |
| StreamProvider | 任意の Stream |
| StateProvider | 任意 |
| ChangeNotifierProvider | ChangeNotifier のサブクラス |
Provider
外部から変更することができないデータを管理するProviderです。

上記のコードでは外部から変更を受けない'Hello World' がProviderで管理するデータになります。ref.watch()でProviderが管理する'Hello World' を取得して画面に表示しています。
StateNotifierProvider
StateNotifier のサブクラスをデータとして管理するProviderです。

まずはStateNotifierProvider で管理するStateNotifier のサブクラスを実装します。
StateNotifier のサブクラスでデータを保持、変更するメソッドを実装します。
上記のコードではint 型の数値をデータとして管理し、管理している数値をインクリメントするincrement() を実装しています。
外部からデータを変更する時はStateNotifier のサブクラスに実装したメソッドを使用します。
StateNotifierProvider を定義する時はStateNotifierProvider の後に<管理するStateNotifier のサブクラス名, StateNotifier で管理するデータの型>が必要なので注意してください。
StateNotifier のサブクラスはWidgetRef クラスのwatch() またはread() の引数にStateNotifierProvider インスタンスの.notifier を指定することで取得することができます。また、StateNotifier のサブクラスのデータ(state )はWidgetRef クラスのwatch() またはread() の引数にStateNotifierProvider インスタンスを指定することで取得することができます。
上記のコードではStateNotifier のサブクラスCounterNotifier クラスに実装したincrement() で、StateNotifier で管理しているデータを変更しています。
Providerで管理しているデータをwatch() で取得した時は、データの変更がウィジェットに通知され、ウィジェットがリビルドされます。フローティングボタンをタップするとデータに変更が入り、RiverpodSample ウィジェットがリビルドされることで画面に表示しているテキストを更新しています。
FutureProvider
Future から取得したデータを管理するProviderです。

WidgetRef クラスのwatch() またはread() の引数にFutureProvider インスタンスを指定することで、AsyncValue<T> 型のインスタンスを取得できます。AsyncValue<T> 型 インスタンスのwhen() は非同期処理の状態に応じたウィジェットを返すことができます。when() のdata 引数には非同期処理が完了した時に表示するウィジェットを返すコールバック関数、loading 引数には非同期処理を実行している時に表示するウィジェットを返すコールバック関数、error 引数には非同期処理に失敗した時に表示するウィジェットを返すコールバック関数をそれぞれ指定します。
上記のコードではFuture.delayed() で非同期処理が開始すると、loading 引数に指定したコールバック関数が実行され、返り値であるインジケータを表示しています。非同期処理が完了した時、data 引数に指定したコールバック関数が実行され、返り値であるText ウィジェットを表示しています。
StreamProvider
Stream から取得したデータを管理するProviderです。

WidgetRef クラスのwatch() またはread() の引数にFutureProvider インスタンスを指定することで、AsyncValue<T> 型のインスタンスを取得できます。AsyncValue<T> 型のインスタンスのwhen() は非同期処理の状態に応じたウィジェットを返すことができます。when() のdata 引数には非同期処理が完了した時に表示するウィジェットを返すコールバック関数、loading 引数には非同期処理を実行している時に表示するウィジェットを返すコールバック関数、error 引数には非同期処理に失敗した時に表示するウィジェットを返すコールバック関数を指定します。
上記のコードではStream.periodic() で非同期処理が開始すると、loading 引数に指定したコールバック関数が実行され、返り値であるインジケータを表示しています。その後は1秒間隔でデータである数値が更新されます。データが更新される度にdata 引数に指定したコールバック関数が実行され、返り値であるText ウィジェットを表示することで数値の変化を画面に表示しています。
StateProvider
外部から変更することができるデータを管理するProviderです。

外部からデータを変更する時はStateController インスタンスのupdate() 、または直接state に変更を加えることでデータを変更します。
StateController インスタンスはWidgetRef クラスのwatch() またはread() の引数に StateProvider インスタンスの.notifier を指定することで取得することができます。また、StateProvider が管理するデータ(state )はWidgetRef クラスのwatch() またはread() の引数にStateProvider インスタンスを指定することで取得することができます。
上記のコードではint 型の数値がProviderで管理するデータになります。StateController インスタンスのstate をインクリメント、つまり管理するデータである数値インクリメントすることでデータを変更しています。
Providerで管理しているデータをwatch() で取得した時は、データの変更がウィジェットに通知され、ウィジェットがリビルドされます。上記のコードではフローティングボタンをタップするとデータに変更が入り、RiverpodSample ウィジェットがリビルドされることで画面に表示しているテキストを更新しています。
ChangedNotifierProvider
ChangeNotifier のサブクラスをデータとして管理するProviderです。

まずはChangeNotifier で管理するChangeNotifier のサブクラスを実装します。
ChangeNotifier のサブクラスでデータを保持、変更するメソッドを実装します。
上記のコードではint 型の数値をデータとして管理し、管理している数値をインクリメントするincrement() を実装しています。
データの更新を監視元に伝えたい時は、notifyListeners()を実行する必要があります。notifyListeners()を記載していないと、データを更新しても監視元に伝わらないので注意してください。
外部からデータを変更する時はChangeNotifier のサブクラスに実装したメソッドを使用します。ChangeNotifier のサブクラスはWidgetRef 型インスタンスのwatch() またはread() の引数にChangedNotifierProvider インスタンスを指定することで取得することができます。
上記のコードではChangeNotifier のサブクラスCounter に実装したincrement() で、Counter で管理しているデータを変更しています。
Providerで管理しているデータをwatch() で取得した時は、データの変更がウィジェットに通知され、ウィジェットがリビルドされます。上記のコードではフローティングボタンをタップするとデータに変更が入り、RiverpodSample ウィジェットがリビルドされることで画面に表示しているテキストを更新しています。
WidgetRefインスタンスの取得方法
通常のウィジェットではWidgetRef インスタンスを使用することができないため、Riverpodで用意されているウィジェットを使用したり、継承する必要があります。
ConsumerWidget
ConsumerWidget を継承したWidgetを定義すると、build() にWidgetRef 型の引数が追加されます。
WidgetRef 型引数のwatch() またはread() の引数にProviderを指定することでProviderからデータを取得することができます。データの変更を監視する必要がある時はwatch() を、監視する必要がない時はread() を使用します。watch() を使用してデータを取得すると、ウィジェット全体がリビルドされます。上記のコードではデータが変更される度にRiverpodSample ウィジェットがリビルドされます。
Consume
Consumer ウィジェットはデータの変更が発生した時のリビルド対象を絞る時に使用します。Cousumer ウィジェットのbuilder 引数に指定したビルダー関数のWidgetRef 型引数のwatch() またはread() の引数にProviderを指定することでProviderからデータを取得することができます。
watch() を使用してデータを取得すると、RiverpodSample ウィジェットがリビルドされるのではなく、Consumer ウィジェットの引数に指定したビルダー関数がリビルドされます。上記のコードではデータが変更される度にConsumer ウィジェットの引数に指定したビルダー関数がリビルドされます。また、Consumer ウィジェットのchild 引数に指定したウィジェットは、ビルダー関数のchild 引数経由で使用することができ、リビルド対象外とすることができます。
データの読み込み
データの変化を監視したり、監視対象を制限することでビルド対象を絞ることができます。
ref.watch()
データの変更を監視したい時はwatch() を使用してデータを取得します。

上記のコードではフローティングボタンをタップするとデータがインクリメントされます。watch() を使用してデータを取得しているので、データに変更が入るとRiverpodSample ウィジェットに変更が通知されます。そしてRiverpodSample ウィジェットがリビルドされることで画面の表示を更新しています。
ref.read()
データの変更を監視する必要がない時はread() を使用してデータを取得します。

上記のコードではフローティングボタンをタップするとデータがインクリメントされます。しかし、read() を使用してデータを取得しているので、その変更がRiverpodSample ウィジェットに通知されません。その結果、RiverpodSample ウィジェットがリビルドされず、画面表示が更新されなくなっています。データは更新したいけど表示の更新は必要ない時に使用します。
ref.select()
データの監視対象を絞る時はselect() を使用してデータを取得します。

上記のコードではユーザの情報(年齢、名前)を管理するStateNotifier を継承したUserStateNotifierクラスです。
上記のコードでは名前と年齢を表示します。また、テキストフィールドをタップすると名前を変更することができ、フローティングボタンをタップすると年齢をインクリメントすることができます。
select() を使用してデータの監視対象を名前だけに絞っているため、テキストフィールド経由で名前を変更すると、表示される名前は更新されますが、年齢の表示は更新されません。年齢の表示が更新されていないことを視覚的にわかりやすくするため、わざと年齢を表示していますが、名前だけを表示する画面仕様だった時、フローティングボタンで年齢をインクリメントしても画面の更新は必要ありません。このように特定のデータの更新だけ監視したい時はselect() が有効です。
Provider修飾子
.family()
外部からパラメータを渡してデータを作成するようにしてくれる修飾子です。
Providerに.family() を付加し、.family() のあとに<データの型, パラメータの型>を指定します。

.autoDispose()
参照されなくなったProviderのデータを破棄してくれる修飾子です。画面遷移した時にProviderが管理しているデータを破棄したい時に使用します。
下記のコードではカウンター画面遷移後にカウントをインクリメントし、元の画面に遷移した後、再度カウンター画面に遷移しても、Providerが管理しているデータは破棄されないので、カウンターのデータはずっと保持されたままです。

StateProvider に.autoDispose を付加してみます。

すると、画面遷移後にProviderが管理しているデータが破棄され、画面遷移を行う度にカウントが0からスタートするようになりました。
このようにProviderに.autoDispose を付加することで参照されなくなったProviderのデータを破棄することができます。
ref.onDispose()
AutoDisposeStateProviderRef の.onDispose を使用することで、Providerのデータを破棄する時、処理を行うことができます。
ref.maintainState
.autoDispose を使用しても、ref.maintainState をtrue にすることで、データを保持し続けることができます。

上記のコードではアプリ起動時に表示されるFirst Page画面のボタンをタップするとNext Page画面に遷移するアプリです。Next Page画面ではFutureProvider を使用して、画面を表示した3秒後に'Hello Future Riverpod' が表示されるようになっています。futureProvider は3秒後に'Hello Future Riverpod' をreturn すると同時にref.maintainState = true を実行してデータを保持するようにしています。つまりFirst Page画面に戻ってもデータは保持されたままなので、再度Next Page画面に遷移してもインジケータは表示されず、遷移直後に'Hello Future Riverpod' が表示されるようになります。

アプリ起動時に表示されるFirst Page画面のボタンをタップしてNext Page画面に遷移した後、'Hello Future Riverpod' が表示される前にFirst Page画面に遷移すると、ref.maintainState = true は実行されず、データは保持されません。その結果、再度Next Page画面に遷移した時はインジケータが表示され、3秒後に'Hello Future Riverpod' が表示されるようになります。
その他
ref.refresh()
ref.refresh でProvifderを強制的に更新することができます。

上記のコードではProviderで現在の日時を管理しています。ボタンをタップすることでProviderを更新し、最新の日時を取得し直して画面に最新の日時を表示するようにしています。
ref.listen()
Providerが持つデータの変更を検知することができます。

上記のコードではフローティングボタンをタップした回数を監視し、5回ごとにアラートを表示させています。
参考文献
Discussion