【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