👶

Riverpod 2.xの使い方

2024/01/11に公開

この記事ではRiverpodの使い方をご紹介します。
現在(2024/1/11)の記法と、それによってどのような挙動をするのか具体的なイメージを述べていますが、内部構造については述べません。勉強の際に参考にさせていただいたサイトを貼ったのでご参考にしてください。

時間が経てば情報は更新されるため、公式サイトを参考に実装を行ってください。
https://riverpod.dev/

また、本記事で使用したコードは以下に記載してあります。
https://github.com/samisami0631/riverpod_playground

もし本記事に内容の誤りがありましたらコメントいただけると嬉しいです。

Riverpodとは

Riverpodとは、便利に異なるウィジェット間で状態管理ができるパッケージです。状態とは、UIを構築するために必要なデータのことです。

Flutterの状態には、単一のWidget内でのみ参照されるephemeral stateと、複数のWidgetから参照されるapp stateがあります。

https://docs.google.com/presentation/d/19SrGm4lhghLsmxMqBt35rCCkd-_ivHrB65D1Gu_wGZI/edit?pli=1#slide=id.g28ce6943ff6_0_348 より図を拝借しました

仮にRiverpodを使わずに状態管理をしたとすると、複雑なアプリではapp stateの変更とapp stateの管理が別のウィジェットで行われるため、関数を渡しまくる必要があり非常にめんどくさいですし、想定しないバグも発生します。

しかし、Riverpodを使用することで、Providerというクラスがデータを一括管理するため、それぞれのウィジェットが状態管理をする必要がなくなり、ウィジェット間でのデータの乖離が生まれなくなります。

使い方概要

以下の順番で使い方を説明します。

  • Riverpodのインストール
  • アプリ呼び出し部分をRiverpodパッケージが提供するウィジェットで囲む
  • ウィジェットでProviderを使えるようにする
  • Providerを作成し、ウィジェットがProviderの値の変化を聞き取ったり、Providerが提供するメソッドを利用してProviderの値を変更する

Riverpodのインストール

flutter pub add flutter_riverpod

また、該当ファイル(以降で記載するファイル)に下記を追加しておきましょう。

import 'package:flutter_riverpod/flutter_riverpod.dart';

アプリ呼び出し部分をRiverpodパッケージが提供するウィジェットで囲む

アプリ全体のすべてのウィジェットがRiverpodの機能を使えるようにするため、このように記載します。

main.dart
void main(){
  runApp(
    const ProviderScope(
      child: App(),
    ),
  );
}

※もしアプリの一部のみがRiverpodを必要としているのであればそこだけ囲めば問題ありません

ウィジェットでProviderを参照できるようにする

  • StatefulWidgetはConsumerStatefulWidgetに書き換えます。また、StateクラスはConsumerStateクラスに書き換えます。なお、ConsumerStateクラスがrefプロパティ(使い方は後述。Providerを参照するときに使う)をグローバルに使用できるようにしているため、refプロパティを渡す必要はありません。
hoge_page.dart
class HogePage extends ConsumerStatefulWidget {
  const HogePage({super.key});
  
  
  ConsumerState<HogePage> createState(){
    return _HogePageState();
  }
}

class _HogePageState extends ConsumerState {
  ...
}
  • StatelessWidgetはConsumerWidgetに書き換えます。なお、ここではrefプロパティをもたせられていないため、buildウィジェットに直接WidgetRef型のパラメータを渡す必要があります。
hoge_page.dart
class HogePage extends ConsumerWidget {
  ...

  
  Widget build(BuildContext context, WidgetRef ref){
    ...
  }
}

これで、それぞれのウィジェットが ref.readref.watch などを使ってProviderの値の変化を監視することが可能になりました(具体的な方法はこのあとすぐ)。

Providerを作成し、ウィジェットがProviderの値の変化を聞き取ったり、Providerが提供するメソッドを利用してProviderの値を変更する

Providerにはいくつかの種類があります。管理したいデータの種類によって扱うProviderが異なります(当然書き方も違う)。それぞれのProviderが扱うデータを最初に述べ、次に使い方を述べていきます。

Providerの種類

  • ①Provider
    • 静的な変化しないデータを扱う場合
    • 別のProviderに依存するデータを扱う場合
  • ②StateProvider:非推奨
    • ユーザーによる入力などの外部からの変更が可能なデータを扱う場合
    • NotifierProviderで簡単にかけるようになったので非推奨
  • ③StateNotifierProvider:非推奨
    • 特定の条件下で変化するような複雑なデータを扱う場合
    • ユーザーによる入力などの外部からの変更が可能なデータと、状態操作メソッドクラスを提供する
    • NotifierProviderで簡単にかけるようになったので非推奨
  • ④ChangeNotifierProvider:非推奨
    • 役割はStateNotifierProviderと同様
    • ミュータブルに変更ができる(メモリの既存アドレスの数字を直接書き換える)が、変更が伝わらない可能性があるため非推奨
  • ⑤NotifierProvider:Riverpod2.0から追加
    • 役割はStateNotifierProviderと同様
    • StateProviderとStateNotifireProviderを簡単に書けるようになっている。推奨
  • ⑥FutureProvider
    • 非同期で取得したデータを扱う場合
    • ユーザーによる入力といった外部からの更新はできない
  • ⑦AsyncNotifierProvider:Riverpod2.0から追加
    • 特定の条件下で変化するような複雑な非同期で取得したいデータを扱う場合
    • ユーザーによる入力などの外部からの変更が可能なデータ(提供する値の型が Future )と、状態操作メソッドクラスを提供する
  • ⑧StreamProvider
    • FutureProviderStream

上記に述べたように、公式は Provider , NotifierProvider , FutureProvider , AsyncNotifierProvider , StreamProvider の使用を推奨しています。レガシーなProviderたちより簡単に書けますし、バグも防げるからです。これらを使っていくようにしましょう。
しかし、技術の過渡期の現在ではすべてのProviderがよく使われていることも事実です。そのため、Riverpod初心者(過去の私)へ向けて、以下ではメモがてらすべてのProviderの使い方に触れます。

①Provider

  • flutter_riverpodパッケージが提供するProviderクラスを利用してProviderを変数に格納する
    • あとでアクセスできるようにするため
    • 設定した変数(作成したProviderクラス)をインスタンス化するとProviderオブジェクトが作成され、ウィジェットの内部からListenできるようになる
provider_page.dart
// 引数には絶対に dynamic Function(ProviderRef<dynamic>) _createFn
// (Providerのrefオブジェクトを引数に受け取る関数)を入れる
// ↑ こいつがflutter_riverpodパッケージに呼び出される
final akeomeProvider = Provider((ref) {

  // 別のProviderの値を参照したいときも以下のように普通に参照すれば良い
  final meals = ref.watch(akeomeProvider);

  // ここでは例として全くデータが変化しないものを挙げている
  return 'あけましておめでとう';
});
  • データをwatchする
    • ここでやっていることは、 akeomeProvider が変更されるたびにこのビルドメソッドを再実行するリスナーを設定し、 akeomeProvider のデータを返す
provider_page.dart
class ProviderPage extends ConsumerState {
  // 引数には絶対に ProviderListenable<T> provider を入れる
  ref.watch(akeomeProvider);
}

このように、異なる画面間で同じデータを使用することができます。

②StateProvider:非推奨

  • flutter_riverpodパッケージが提供するStateProviderクラスを利用しStateProviderを変数に格納する
    • あとでアクセスできるようにするため
    • 設定した変数(作成したStateProviderクラス)をインスタンス化するとStateProviderオブジェクトが作成され、ウィジェットの内部からListenできるようになる
state_provider_page.dart
// 引数には絶対に dynamic Function(ProviderRef<dynamic>) _createFn
// (Providerのrefオブジェクトを引数に受け取る関数)を入れる
// ↑ こいつがflutter_riverpodパッケージに呼び出される
final countProvider = StateProvider((ref) {
  return 0;
});
  • データをwatchする
    • ここでやっていることは、 countProvider が変更されるたびにこのビルドメソッドを再実行するリスナーを設定し、 countProvider のデータを返す
    • watch推奨。ここでwatchではなくreadにすると
      • そのページ内で変更しても変わらない
      • 別ページから戻ってきた時にはじめてデータが更新される
state_provider_page.dart
class StateProviderPage extends ConsumerState {
  // 引数には絶対に ProviderListenable<T> provider を入れる
  ref.watch(countProvider);
}
  • メソッドをreadする
    • readでよいのは、データが更新されるたびに再ビルドが必要なメソッドではなく、常に同じメソッドであってよいから
state_provider_page.dart
class StateProviderPage extends ConsumerWidget {
  ...

  
  Widget build(BuildContext context, WidgetRef ref){
    return IconButton(
      onPressed() {
        // countProvider.notifier で StateNotifierクラスを呼び出せる
        // StateNotifireProvider(後述)と違うのは、関数を↓でべたがきする必要があること
        ref.read(favoriteMealsProvider.notifier).state++
      }
}

このように、異なる画面間で同じデータを使用することができます。それぞれの画面で数字を増やしたりリセットしても、同じものが扱われています。

③StateNotifierProvider:非推奨

  • 初期データとデータを編集するメソッドを持つ、StateNotifierクラスを拡張させた独自のクラスを作成する
    • StateNotifierクラスはインスタンス化されるわけではない
state_notifier_provider_page.dart
// 名前の最後にNotifierをつけるのが慣習
// 何のデータを管理しようとしているのかを定義
class CountNotifier extends StateNotifier<int> {
  // 初期値を設定する
  CountNotifier(): super([]);

  // このデータを編集するためのメソッドを追加
  void increment(){
  // StateNotifierを使用する場合メモリ内の既存の値を編集することはできないため、
  // Notifierで管理されている値は決して編集できない(=CountNotifier++とかはできない)
  // → そのため必ず新規に作成する必要がある
  // データを保持しているグローバルに利用できるstateプロパティ(StateNotifierでのみ使用可能)
  // に新しいデータを代入
    state++;
}
  • flutter_riverpodパッケージが提供するStateNotifierProviderクラスを利用してStateNotifierProviderを変数に格納する
    • あとでアクセスできるようにするため
    • StatefulWidgetがStateWidgetと連携するように、StateNotifierクラスを拡張させた独自のクラス(先ほど作成したクラス)と連携させる
state_notifier_provider_page.dart
// インスタンス化したStateNotifier(CountNotifier)を参照するし、
// 初期データ(int型)を返すので、型を指定しておく
final countProvider = StateNotifierProvider<CountNotifier, int>((ref){
  return CountNotifier();
})
  • データをwatchする
    • ここでやっていることは、 countProvider が変更されるたびにこのビルドメソッドを再実行するリスナーを設定し、 countProvider のデータを返す
    • watch推奨。ここでwatchではなくreadにすると
      • そのページ内で変更しても変わらない
      • 別ページから戻ってきた時にはじめてデータが更新される
state_notifier_provider_page.dart
class StateNotifierProviderPage extends ConsumerState {
  // 引数には絶対に ProviderListenable<T> provider を入れる
  ref.watch(countProvider);
}
  • メソッドをreadする
    • readでよいのは、データが更新されるたびに再ビルドが必要なメソッドではなく、常に同じメソッドであってよいから
    • ちなみに、StateProviderみたいにべたがきしても一応機能する。invalid_use_of_visible_for_testing_number でるけど
state_notifier_provider_page.dart
class MealDetailScreen extends ConsumerWidget {
  ...

  
  Widget build(BuildContext context, WidgetRef ref){
    return IconButton(
      onPressed() {
        // countProvider.notifier で StateNotifierクラスを呼び出せる
        ref.read(countProvider.notifier).increment();
        ...
      }
    );
  }
}

このように、異なる画面間で同じデータを使用することができます。それぞれの画面で数字を増やしたりリセットしても、同じものが扱われています。

④ChangeNotifierProvider:非推奨

ミュータブルに変更ができる(既存アドレスのメモリの数字を直接書き換える)が、変更が伝わらない可能性があり、非推奨です。すでにほぼ使われていないようなので、割愛します。

⑤NotifierProvider:Riverpod2.0から追加

  • 初期データとデータを編集するメソッドを持つ、Notifierクラスを拡張させた独自のクラスを作成する
    • Notifierクラスはインスタンス化されるわけではない
notifier_page.dart
// 名前の最後にNotifierをつけるのが慣習
// 何のデータを管理しようとしているのかを定義
class CountNotifier extends Notifier<int> {
  // 初期値を設定する
  
  int build() {
    return 0;
  }

  void increment() {
    state++;
  }

  void reset() {
    state = 0;
  }
}
  • flutter_riverpodパッケージが提供するNotifierProviderクラスを利用してNotifierProviderを変数に格納する
    • あとでアクセスできるようにするため
    • StatefulWidgetがStateWidgetと連携するように、Notifierクラスを拡張させた独自のクラス(先ほど作成したクラス)と連携させる
// インスタンス化したNotifier(CountNotifier)を参照するし、
// 初期データ(int型)を返すので、型を指定しておく
final countProvider = NotifierProvider<CountNotifier, int>(() {
  return CountNotifier();
});
  • データをwatchする
    • ここでやっていることは、 countProvider が変更されるたびにこのビルドメソッドを再実行するリスナーを設定し、 countProvider のデータを返す
    • watch推奨。ここでwatchではなくreadにすると
      • そのページ内で変更しても変わらない
      • 別ページから戻ってきた時にはじめてデータが更新される
notifier_provider_page.dart
class NotifierProviderPage extends ConsumerState {
  // 引数には絶対に ProviderListenable<T> provider を入れる
  ref.watch(countProvider);
}
  • メソッドをreadする
    • readでよいのは、データが更新されるたびに再ビルドが必要なメソッドではなく、常に同じメソッドであってよいから
    • ちなみに、StateProviderみたいにべたがきしても一応機能する。invalid_use_of_visible_for_testing_number でるけど。
notifier_provider_page.dart
class NotifierProviderPage extends ConsumerWidget {
  ...

  
  Widget build(BuildContext context, WidgetRef ref){
    return IconButton(
      onPressed() {
        // countProvider.notifier で Notifierクラスを呼び出せる
        ref.read(countProvider.notifier).increment();
	...
      }
    );
  }
}

このように、異なる画面間で同じデータを使用することができます。それぞれの画面で数字を増やしたりリセットしても、同じものが扱われています。

⑥FutureProvider

  • flutter_riverpodパッケージが提供するFutureProviderクラスを利用してFutureProviderを変数に格納する
    • あとでアクセスできるようにするため
    • 設定した変数(作成したFutureProviderクラス)をインスタンス化するとFutureProviderオブジェクトが作成され、ウィジェットの内部からListenできるようになる
future_provider_page.dart
// 引数には絶対に dynamic Function(ProviderRef<dynamic>) _createFn
// (Providerのrefオブジェクトを引数に受け取る関数)を入れる
// ↑ こいつがflutter_riverpodパッケージに呼び出される
final delayFetchProvider = FutureProvider((ref) {
  (ref) async {
    await Future.delayed(const Duration(seconds: 3));
    return 'あけましておめでとう';
  },
});
  • データをwatchする
    • ここでやっていることは、 delayFetchProvider が変更されるたびにこのビルドメソッドを再実行するリスナーを設定し、 delayFetchProvider のデータを返す
    • FutureProviderを作成するとAsyncValueオブジェクトが生成されるため、それに応じて表示を制御する
      • AsyncValue は非同期通信の通信中、通信終了、異常終了処理をハンドリングしてくれる便利な機能のこと
future_provider_page.dart
class FutureProviderPage extends ConsumerState {
  // 引数には絶対に ProviderListenable<T> provider を入れる
  ref.watch(delayFetchProvider).when(
    error: (err, _) => Text(err.toString()), //エラー時
    loading: () => const CircularProgressIndicator(), //読み込み時
    data: (data) => Text(data.toString()), //データ受け取り時
  ),
}
  • データをrefreshする
    • なお、同じデータだとAsyncDataであり続けるためローディング表示をするのであればisLoadingの値を見るのが最適
future_provider_page.dart
class FutureProviderPage extends ConsumerWidget {
  ...

  
  Widget build(BuildContext context, WidgetRef ref){
    return IconButton(
      onPressed() {
	// AsyncData<String>(isLoading: true, value: あけましておめでとう) から
	// AsyncData<String>(value: あけましておめでとう) になるだけ
	ref.refresh(delayFetchProvider);
	...
      }
    );
  }
}

このように、異なる画面間で同じデータを使用することができます。それぞれの画面で数字を増やしたりリセットしても、同じものが扱われています。

⑦AsyncNotifierProvider:Riverpod2.0から追加

  • 初期データとデータを編集するメソッドを持つ、AsyncNotifierクラスを拡張させた独自のクラスを作成する
    • AsyncNotifierクラスはインスタンス化されるわけではない
async_notifier_provider_page.dart
// 名前の最後にNotifierをつけるのが慣習
// 何のデータを管理しようとしているのかを定義
class CountNotifier extends AsyncNotifier<int> {
  // 初期値を設定する
  
  FutureOr<int> build() {
    return 0;
  }

  // このデータを編集するためのメソッドを追加
  // stateはAsyncValue型であり、それぞれの値に対して明記しなくてはいけないことに注意
  Future<void> increment(BuildContext context) async{
    // ここではローディング表示をしてみたいのでAsyncLoadingに変更
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() async {
      await Future.delayed(const Duration(seconds: 3));
      const data = 3;
      return data;
    });
  }
}
  • flutter_riverpodパッケージが提供するAsyncNotifierProviderクラスを利用してAsyncNotifierProviderを変数に格納する
    • あとでアクセスできるようにするため
    • StatefulWidgetがStateWidgetと連携するように、AsyncNotifierクラスを拡張させた独自のクラス(先ほど作成したクラス)と連携させる
async_notifier_provider_page.dart
// インスタンス化したAsyncNotifier(CountNotifier)を参照するし、
// 初期データ(List<Meal>型)を返すので、型を指定しておく
final countProvider = 
  AsyncNotifierProvider<CountNotifier, int>(CountNotifier.new);
  • データをwatchする
    • ここでやっていることは、 countProvider が変更されるたびにこのビルドメソッドを再実行するリスナーを設定し、 countProvider のデータを返す
    • watch推奨。ここでwatchではなくreadにすると
      • そのページ内で変更しても変わらない
      • 別ページから戻ってきた時にはじめてデータが更新される
async_notifier_provider_page.dart
class AsyncNotifierProviderPage extends ConsumerState {
  // 引数には絶対に ProviderListenable<T> provider を入れる
  ref.watch(countProvider);
}
  • メソッドをreadする
    • readでよいのは、データが更新されるたびに再ビルドが必要なメソッドではなく、常に同じメソッドであってよいから
    • ちなみに、StateProviderみたいにべたがきしても一応機能する。invalid_use_of_visible_for_testing_number でるけど。
async_notifier_provider_page.dart
class AsyncNotifierProviderPge extends ConsumerWidget {
  ...

  
  Widget build(BuildContext context, WidgetRef ref){
    return IconButton(
      onPressed() {
        // countProvider.notifier で AsyncNotifierクラスを呼び出せる
        ref.read(countProvider.notifier).increment(context);
        ...
      }
    );
}

このように、異なる画面間で同じデータを使用することができます。それぞれの画面で数字を増やしたりリセットしても、同じものが扱われています。

⑧StreamProvider

FutureProviderStream 版であり、 FutureProvider と全く同じ使い方をするので割愛します。

Riverpod generatorの使い方を説明する前に、ここまでのまとめ

以上で全Providerの紹介は以上です。
ここで、Riverpod generatorの使い方を知る前に、公式がRiverpod2.xで推奨しているProviderについて、表でまとめておきましょう。

Provider名 メソッド込み メソッドなし
①Provider
⑤NotifierProvider
⑥FutureProvider
⑦AsyncNotifierProvider
⑧StreamProvider

Riverpod2.xから導入されたRiverpod generatorでは、メソッドの有無で書き方が異なります。次の節から説明します。

Riverpod generatorのインストール

flutter pub add riverpod_annotation

また、該当ファイル(以降で記載するファイル)に下記を追加しましょう。

example.dart
import 'package:riverpod_annotation/riverpod_annotation';

// ファイル名.g.dart
part example.g.dart'

メソッド込みクラスのRiverpod generatorの書き方

NotifierProvierAsyncNotifierProvider に適用されます。公式では「Class-Based」と表現されています。
NotifierProvider では Notifier クラスに初期データと編集するメソッドを書き、それをNotifierProviderクラスが参照していました。
AsyncNotifierProvider では AsyncNotifier クラスに初期データと編集するメソッドを書き、それを AsyncNotifierProvider クラスが参照していました。
以上の2つの書き方をgeneratorでよしなにまとめてこのように書くことができます。


class Count extends _$Count {
  
  // AsyncNotifierProviderを書きたいのであれば、Int→Future<Int>
  Int build() {
    return 0;
  }

  // メソッドを定義
  void increment() {
    state++;
  }
}

メソッドなしクラスのRiverpod generatorの書き方

ProviderFutureProviderStreamProvider に適用されます。公式では「Functional」と表現されています。
いずれのProviderでも、それぞれのProviderクラスにデータを書いていました。この書き方をgeneratorでよしなにまとめてこのように書くことができます。


// 型はよしなに変更してね
String akeome(AkeomeRef ref) {
  return 'akeome';
}

自動生成ファイルの作成

dart run build_runner watch --delete-conflicting-outputs

まとめ

Riverpodの2024/1/11現在の使い方を述べました。ご参考になれば幸いです。
状態管理の方法も日々変化しているため、一次情報をあたって実装を進めましょう。

Discussion