🙌

【Flutter】Riverpodプロバイダーの種類一覧!コード生成プロバイダー辞書

に公開

はじめに


皆さん、こんにちは。

Riverpodを使い始めたばかりで、その「お作法」にまだ慣れていない方や、特定のケースでどのプロバイダーをどう書けばよいか確認したい方のための、辞書的な役割も目指しました。この記事が、皆さんのRiverpodに対する「???」を解消する一助となれば幸いです。

ここで掲載するコードの構成
プロバイダー定義の関数やクラス

プロバイダーの値を利用するウィジェット

実行画面は次の通り、それぞれのプロバイダーを利用しているウィジェットを縦に並べただけになっています。

サンプルコード

https://github.com/peter-norio/flutter/tree/main/riverpod2_generator_sample

関数プロバイダー

概要

  • 最もシンプルな形式
  • 読み取り専用の値を提供する
  • アプリ全体で固定的な値を共有する
    • URLや設定値など
  • Flutter Riverpod Snippetsのキーワード「riverpod」

関数プロバイダーは、最もシンプルで基本的なプロバイダです。主な役割は、アプリ内で変更されることのない「読み取り専用の値」を提供することです。

このプロバイダーは、一度だけ値(オブジェクトやデータ)を生成し、その結果をキャッシュして再利用します。そのため、外部から状態を変更する方法を提供しません。

具体的な使い道としては、アプリ全体で共有したい固定的な値を公開するのに最適です。例えば、APIのベースURL、リポジトリクラスのインスタンス、またはアプリ全体で共通の設定値やテーマカラーといった、一度定義したら変わらない値を扱う場合に役立ちます。

このように、関数プロバイダーは「一度生成した値やオブジェクトを、どこからでも安全に読み取れるようにする」という、目的で利用されます。

書き方

関数プロバイダーは、@riverpodアノテーションを付けた関数を定義するだけで作成できます。

関数の真上に@riverpodと記述します。これがコード生成の目印です。通常のDart関数と同様に、プロバイダ名にしたい関数名(例: hello)と、提供したい値の型(例: String)を定義します。

プロバイダーで提供したい値をreturnで返します。例では'Hello Riverpod'という文字列を返しており、これがプロバイダーを通じて読み取れる値となります。


String hello(Ref ref) {
  return 'Hello Riverpod';
}

スニペットでの記述

Flutter Riverpod Snippetsのキーワード「riverpod」で雛形を生成できます。関数名や具体的な戻り値は未入力になっているので、追記します。

雛形

 (Ref ref) {
  return ;
}

プロバイダー定義とウィジェットでの利用例

同じファイル内で、プロバイダーとそれを利用するウィジェットを記述したものを掲載します。

コードのコメントはAIでつけました。

/use_generator/simple/fn_hello.dart
// FlutterのUIコンポーネントをインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を使用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Riverpodのコード生成アノテーションを使用するためにインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';

// コード生成されたファイル(`fn_hello.g.dart`)をインポートします。このファイルは`@riverpod`アノテーションに基づいて自動生成されます。
part 'fn_hello.g.dart';

// hello関数をRiverpodのプロバイダとして定義することを示すアノテーションです。

// `hello`という名前のプロバイダを定義します。このプロバイダは文字列を返します。
String hello(Ref ref) {
  // `Ref`オブジェクトは、他のプロバイダにアクセスしたり、プロバイダのライフサイクルを管理したりするために使用されますが、この例では直接使用していません。
  // プロバイダが提供する値です。
  return 'Hello Riverpod';
}

// Riverpodのプロバイダを読み取るためのウィジェットを定義します。`ConsumerWidget`は、`WidgetRef`にアクセスするために使用されます。
class GeneratorHello extends ConsumerWidget {
  const GeneratorHello({super.key});

  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef`を使用してプロバイダにアクセスします。
  Widget build(BuildContext context, WidgetRef ref) {
    // `helloProvider`を監視(watch)し、その値を取得します。`helloProvider`は、`@riverpod`アノテーションによって自動生成されたプロバイダです。
    // `watch`は、プロバイダの値が変更されるとウィジェットを再構築します。
    final hello = ref.watch(helloProvider);
    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text('(geneあり)関数:'),
        // `helloProvider`から取得した文字列を表示するテキストウィジェットです。
        Text(hello),
      ],
    );
  }
}
参考リンク集

クラスプロバイダー

概要

  • 変更可能な値と変更方法を提供する
  • アプリ全体で動的な値を共有できる
  • 複数のコンポーネントで動きを連動させる
    • 一覧表示のデータやログイン状態など
  • Flutter Riverpod Snippetsのキーワード「riverpodClass」

クラスプロバイダー(NotifierProvider)は、「変更可能な値(状態)」と「その値を変更するためのロジック(メソッド)」をセットで提供します。

このプロバイダーは、アプリ内でユーザーの操作などに応じて変化する「動的な値」を管理するのに適しており、ウィジェット側からプロバイダが公開するメソッドを呼び出すことで、状態を更新できます。

具体的な使い道としては、複数のウィジェットで動きを連動させたい場合に最適です。例えば、ユーザーの「ログイン状態」、検索や追加・削除ができる「一覧表示のデータ」、入力フォームの状態管理などが典型的な例です。

ある場所で状態が変更されると、その状態を監視している全てのUIが一斉に更新されるため、アプリ全体で一貫した状態を簡単に保つことができます。

書き方

クラスプロバイダーは、@riverpodアノテーションを付けたクラスを定義して作成します。

クラスの真上に@riverpodと記述します。次に、class クラス名 extends _$クラス名のように、自動生成される_$クラス名を継承したクラスを作成します。この_$付きのクラスに状態管理に関する機能が搭載されています。

クラス内では、buildメソッドをオーバーライドして状態の初期値を返します。

最後に、状態を変更するためのメソッド(例: increment)を定義します。メソッド内では、特別な変数stateに新しい値を代入する(state = state + 1;)だけで、状態が更新され、関連するUIが自動で再描画されます。


class CounterNotifier extends _$CounterNotifier {
  
  int build() {
    return 0;
  }

  void increment() {
    state = state + 1;
  }
}

スニペットでの記述

Flutter Riverpod Snippetsのキーワード「riverpodClass」で雛形を生成できます。クラス名やbuildメソッドの実装は未入力になっているので、追記します。状態更新用のメソッドも合わせて追記します。

雛形

class  extends _$ {
  
   build() {
    return ;
  }
}

プロバイダー定義とウィジェットでの利用例

同じファイル内で、プロバイダーとそれを利用するウィジェットを記述したものを掲載します。

コードのコメントはAIでつけました。

/use_generator/simple/class_counter.dart
// FlutterのUIコンポーネントをインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を使用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Riverpodのコード生成アノテーションを使用するためにインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';

// コード生成されたファイル(`class_counter.g.dart`)をインポートします。このファイルは`@riverpod`アノテーションに基づいて自動生成されます。
part 'class_counter.g.dart';

// CounterNotifierクラスをRiverpodのプロバイダとして定義することを示すアノテーションです。

// `CounterNotifier`は、コード生成によって`_`が接頭辞として付いた`_$CounterNotifier`を継承します。
// これにより、`StateNotifier`の機能が提供されます。
class CounterNotifier extends _$CounterNotifier {
  // プロバイダの初期状態を定義するメソッドです。
  // この場合、カウンターの初期値は0です。
  
  int build() {
    return 0;
  }

  // カウンターの値を1増やすメソッドです。
  // `state`は`StateNotifier`が保持する現在の状態(int型)を表します。
  void increment() {
    state = state + 1;
  }
}

// Riverpodのプロバイダを読み取るためのウィジェットを定義します。
// `ConsumerWidget`は、`WidgetRef`にアクセスし、プロバイダの状態変化に反応するために使用されます。
class GeneratorCounter extends ConsumerWidget {
  // コンストラクタです。`super.key`はウィジェットの一意性を識別するために使用されます。
  const GeneratorCounter({super.key});

  // ウィジェットのUIを構築するメソッドです。
  // `WidgetRef`を使用してプロバイダにアクセスします。
  
  Widget build(BuildContext context, WidgetRef ref) {
    // `counterNotifierProvider`を監視(watch)し、その現在の状態(int型のカウンター値)を取得します。
    // `watch`を使用することで、`counterNotifierProvider`の状態が変更されるたびにこのウィジェットが再構築されます。
    final counter = ref.watch(counterNotifierProvider);

    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text('(geneあり)クラス :'),
        // `counterNotifierProvider`から取得した現在のカウンター値を表示するテキストウィジェットです。
        Text('$counter'),
        SizedBox(
          width: 16,
        ),
        ElevatedButton(
            onPressed: () {
              // `counterNotifierProvider`の`notifier`(`CounterNotifier`のインスタンス)を取得し、
              // その`increment`メソッドを呼び出してカウンターの値を増やします。
              // `read`は一度だけプロバイダのインスタンスを取得し、変更を監視しません。
              ref.read(counterNotifierProvider.notifier).increment();
            },
            child: Text('+'))
      ],
    );
  }
}
参考リンク集

パラメーターを受け取る

概要

  • プロバイダーの処理で外部から値が必要な場合に利用
  • プロバイダー内で利用する値を利用時に渡せる
    • プロバイダーの初期値として返す
    • 渡された値でデータを取得する
  • ウィジェットが持つデータを渡す
    • ユーザーからの入力値や選択
    • 詳細画面へ遷移し表示するデータのID
  • Flutter Riverpod Snippetsのキーワードはなし

「パラメータを受け取るプロバイダー」として定義することで、プロバイダーの処理に外部の値が必要な場合は、利用時に渡たすことができます。これにより、プロバイダの再利用性が大幅に向上します。

プロバイダーをwatchreadする際に引数を渡すことで、その値をプロバイダー内で利用できます。例えば、渡された値をそのまま初期値として返したり、その値を元にAPIを叩いて特定のデータを取得したりすることが可能です。

この「渡す値」は、ウィジェットが持つ動的なデータであることがほとんどです。

典型的な例として、ユーザーが入力した検索キーワードや、一覧画面で選択した項目のIDなどが挙げられます。詳細画面へ遷移する際に、表示したいデータのIDをプロバイダに渡して、そのIDに対応する情報だけを通信で取得するといった使い方ができます。

関数プロバイダーの場合

パラメータを受け取る関数プロバイダーは、通常の関数プロバイダーの定義に引数を追加するだけで簡単に作成できます。

@riverpodを付けた関数の引数リストに、必須のRef refの後ろへ、受け取りたいパラメータを追加するだけです。

このように定義すると、UI側でこのプロバイダを呼び出す際にref.watch(helloParamProvider("Taro"))のように、引数を渡すことが必須になります。

プロバイダの内部では、渡された値(この例ではuser)を、returnする値を動的に生成したり、データを取得する際の条件として自由に利用したりできます。


String helloParam(Ref ref, String user) {
  return 'Hello $user';
}

プロバイダー定義とウィジェットでの利用例

同じファイル内で、プロバイダーとそれを利用するウィジェットを記述したものを掲載します。

コードのコメントはAIでつけました。

/use_generator/param/fn_param_hello.dart
// FlutterのUIコンポーネントをインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を使用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Riverpodのコード生成アノテーションを使用するためにインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';

// コード生成されたファイル(`fn_param_hello.g.dart`)をインポートします。このファイルは`@riverpod`アノテーションに基づいて自動生成されます。
part 'fn_param_hello.g.dart';

// `helloParam`関数をRiverpodのプロバイダとして定義することを示すアノテーションです。
// このプロバイダは引数を受け取ります。

// `helloParam`という名前のプロバイダを定義します。
// このプロバイダは文字列と`user`という名前のString型引数を受け取ります。
String helloParam(Ref ref, String user) {
  // プロバイダが提供する値です。引数`user`を埋め込んだ文字列を返します。
  return 'Hello $user';
}

// Riverpodのプロバイダを読み取るためのウィジェットを定義します。
// `ConsumerWidget`は、`WidgetRef`にアクセスするために使用されます。
class ParamHello extends ConsumerWidget {
  const ParamHello({super.key});

  // 親クラスのメソッドをオーバーライドすることを示します。
  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef`を使用してプロバイダにアクセスします。
  Widget build(BuildContext context, WidgetRef ref) {
    // `helloParamProvider`を監視(watch)し、`'John'`を引数として渡してその値を取得します。
    // `helloParamProvider`は、`@riverpod`アノテーションによって自動生成された`family`プロバイダです。
    // `watch`は、プロバイダの値が変更されるとウィジェットを再構築します。
    final helloUser = ref.watch(helloParamProvider('John'));

    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text('(geneあり)関数 引数:'),
        // `helloParamProvider`から取得した文字列を表示するテキストウィジェットです。
        Text(helloUser),
      ],
    );
  }
}

クラスプロバイダーの場合

パラメータを受け取るクラスプロバイダーは、buildメソッドに引数を追加することで作成します。

@riverpodを付けたクラス内のbuildメソッドの引数に、受け取りたいパラメータを定義するだけです。

このように定義すると、UI側でこのプロバイダーを呼び出す際にref.watch(counterParamNotifierProvider(10))のように、buildメソッドに対応する引数を渡すことが必須になります。

buildメソッドに渡された値は、そのプロバイダーの初期化処理に利用されます。初期値としてそのまま使用したり、初期値を生成するための材料にするなどで利用されます。


class CounterParamNotifier extends _$CounterParamNotifier {
  
  int build(int count) {
    return count;
  }

  void increment() {
    state = state + 1;
  }
}

プロバイダー定義とウィジェットでの利用例

同じファイル内で、プロバイダーとそれを利用するウィジェットを記述したものを掲載します。

コードのコメントはAIでつけました。

/use_generator/param/class_param_counter.dart
// FlutterのUIコンポーネントをインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を使用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Riverpodのコード生成アノテーションを使用するためにインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';

// コード生成されたファイル(`class_param_counter.g.dart`)をインポートします。このファイルは`@riverpod`アノテーションに基づいて自動生成されます。
part 'class_param_counter.g.dart';

// `CounterParamNotifier`クラスをRiverpodのプロバイダとして定義することを示すアノテーションです。
// このクラスプロバイダは引数を受け取ります。

// `CounterParamNotifier`は、コード生成によって`_`が接頭辞として付いた`_$CounterParamNotifier`を継承します。
// これにより、`StateNotifier`の機能が提供されます。
class CounterParamNotifier extends _$CounterParamNotifier {
  // プロバイダの初期状態を定義するメソッドです。
  // このメソッドは`count`というint型引数を受け取り、それを初期値として返します。
  
  int build(int count) {
    return count;
  }

  // カウンターの値を1増やすメソッドです。
  // `state`は`StateNotifier`が保持する現在の状態(int型)を表します。
  void increment() {
    state = state + 1;
  }
}

// Riverpodのプロバイダを読み取るためのウィジェットを定義します。
// `ConsumerWidget`は、`WidgetRef`にアクセスし、プロバイダの状態変化に反応するために使用されます。
class ParamCounter extends ConsumerWidget {
  const ParamCounter({super.key});

  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef`を使用してプロバイダにアクセスします。
  Widget build(BuildContext context, WidgetRef ref) {
    // `counterParamNotifierProvider`のインスタンスを生成します。
    // 引数として`10`を渡すことで、カウンターの初期値が10に設定されたプロバイダが取得されます。
    // この`counterNotifier`はプロバイダの参照であり、まだ実際の値ではありません。
    final counterNotifier = counterParamNotifierProvider(10);
    // `counterNotifier`プロバイダの状態を監視(watch)し、その現在の値(int型のカウンター値)を取得します。
    // `watch`を使用することで、プロバイダの状態が変更されるたびにこのウィジェットが再構築されます。
    final counter = ref.watch(counterNotifier);

    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text('(geneあり)クラス 引数:'),
        // `counterNotifier`プロバイダから取得した現在のカウンター値を表示するテキストウィジェットです。
        Text('$counter'),
        // 子ウィジェット間の水平方向のスペースを追加します。
        SizedBox(
          width: 16,
        ),
        // ボタンウィジェットを定義します。
        ElevatedButton(
            onPressed: () {
              // `counterNotifier`プロバイダの`notifier`(`CounterParamNotifier`のインスタンス)を取得し、
              // その`increment`メソッドを呼び出してカウンターの値を増やします。
              // `read`は一度だけプロバイダのインスタンスを取得し、変更を監視しません。
              ref.read(counterNotifier.notifier).increment();
            },
            child: Text('+'))
      ],
    );
  }
}
参考リンク集

Futureプロバイダー

概要

  • 非同期処理を扱うプロバイダー
  • 非同期処理の進行状況に応じた値を利用可能
    • 待機中、結果データ、エラーデータ
  • AsyncValue型の値からwhenメソッドを使って値を利用
    • loading, data, error
  • 非同期処理の複雑な記述をウィジェットから分離できる
  • Flutter Riverpod Snippetsのキーワード
    • 関数プロバイダーの場合「riverpodFuture」
    • クラスプロバイダーの場合「riverpodAsyncClass」

FutureProviderは、APIからのデータ取得のような非同期処理を専門に扱うプロバイダーです。このプロバイダーの最大の特長は、非同期処理の進行状況に応じた値を、ウィジェット側で利用できることです。

値はAsyncValueという特別な型でラップされており、「待機中(loading)・成功データ(data)・エラー(error)」という3つの状態を表現します。

ウィジェット側では、このAsyncValueが持つ.when()メソッドを使うことで、それぞれの状態に応じた表示の出し分けを簡単に行えます。

.when()にはloading, data, errorの3つのコールバックを渡し、状態に合ったウィジェットを返すだけです。

この仕組みにより、複雑な状態管理の記述をウィジェットから分離でき、コードをクリーンに保つことができます。

関数プロバイダーの場合

FutureProviderを作成するには、@riverpodアノテーションを付け、戻り値の型をFutureにしたasync関数を定義します。

書き方のポイントは2つです。まず、関数の戻り値の型を、Future<String>のようにFutureでラップします。次に関数にasyncキーワードを付けます。

この定義だけで、Riverpodは自動的にこのプロバイダをFutureProviderとして扱ってくれます。

関数の本体では、awaitを使ってAPI通信やデータベースアクセスなどの非同期処理を記述します。処理が成功した場合にreturnした値(例: 'Hello Future')が、UI側でAsyncValuedataとして渡されます。処理中にエラーが発生した場合は、自動でerror状態としてUIに通知されます。


Future<String> futureHello(Ref ref) async {
  await Future.delayed(Duration(seconds: 3));
  return 'Hello Future';
}

スニペットでの記述

Flutter Riverpod Snippetsのキーワード「riverpodFuture」で雛形を生成できます。関数名や型パラメータ、具体的な戻り値は未入力になっているので、追記します。

雛形

FutureOr<> (Ref ref) {
  return ;
}

プロバイダー定義とウィジェットでの利用例

同じファイル内で、プロバイダーとそれを利用するウィジェットを記述したものを掲載します。

コードのコメントはAIでつけました。

/use_generator/future/fn_future_hello.dart
// FlutterのUIコンポーネントをインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を使用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Riverpodのコード生成アノテーションを使用するためにインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';

// コード生成されたファイル(`fn_future_hello.g.dart`)をインポートします。このファイルは`@riverpod`アノテーションに基づいて自動生成されます。
part 'fn_future_hello.g.dart';

// `futureHello`関数をRiverpodのプロバイダとして定義することを示すアノテーションです。
// このプロバイダは非同期的に文字列を返します。

// `futureHello`という名前のプロバイダを定義します。
// このプロバイダは`Future<String>`を返し、非同期処理を伴います。
Future<String> futureHello(Ref ref) async {
  // 3秒間の遅延をシミュレートします。これは非同期処理(例: ネットワーク通信)を模倣しています。
  await Future.delayed(Duration(seconds: 3));
  // 非同期処理が成功した場合に返す文字列です。
  return 'Hello Future';
}

// Riverpodのプロバイダを読み取るためのウィジェットを定義します。
// `ConsumerWidget`は、`WidgetRef`にアクセスし、プロバイダの状態変化に反応するために使用されます。
class FutureHello extends ConsumerWidget {
  const FutureHello({super.key});

  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef`を使用してプロバイダにアクセスします。
  Widget build(BuildContext context, WidgetRef ref) {
    // `futureHelloProvider`を監視(watch)し、その値を`AsyncValue<String>`型で取得します。
    // `watch`は、プロバイダの値が変更されるとこのウィジェットを再構築します。
    final AsyncValue<String> hello = ref.watch(futureHelloProvider);

    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text('(geneあり)関数 Future:'),
        // `AsyncValue`の`when`メソッドを使って、非同期処理の現在の状態に応じて異なるUIを表示します。
        hello.when(
            // 非同期処理が「ロード中」の状態の場合に表示されるウィジェットです。
            loading: () => Text('ロード中'),
            // 非同期処理が成功し「データ」が取得できた状態の場合に表示されるウィジェットです。
            data: (hello) => Text(hello),
            // 非同期処理中に「エラー」が発生した場合に表示されるウィジェットです。
            error: (e, st) => Text(e.toString())),
      ],
    );
  }
}

クラスプロバイダーの場合

状態を変更できるFutureProviderは、buildメソッドをasyncにし、戻り値をFuture型(またはFutureOr型)にすることで作成します。

buildメソッドでは、API通信などの非同期処理を行い、その結果を初期値として返します。この処理中は、プロバイダーの状態は自動的に「待機中」になります。

非同期プロバイダーにおいて、state プロパティは非同期処理の現在の状態を表します。非同期処理の状態は「待機中」「エラー」「現在の値」の3種類です。

stateAsyncValue.loading()AsyncValue.data()AsyncValue.error() のいずれかを代入すると、その変更は ref.watch() で監視しているウィジェットに即座に伝わり、プロバイダーを利用しているウィジェットの AsyncValue.when() メソッドが対応するUIに反応して更新されます。

状態を更新するメソッドでは、現在のstateの値を確認した上で更新処理を行います。現在の値はstate.valueOrNullで取得します。

現在の状態を確認するのは、状態更新処理を呼び出した時に状態が「待機中」や「エラー」だった場合は「現在の値」が存在しないため、処理を進めるとエラーになってしまうためです。return とだけするとstate は何も変わらず、直前の状態を維持します。


class FutureCounterNotifier extends _$FutureCounterNotifier {
  // 補完だとFutureOr型だった
  
  Future<int> build() async {
    await Future.delayed(Duration(seconds: 3));
    return 0;
  }

  void increment() async {
    final currentValue = state.valueOrNull;
    if (currentValue == null) {
      return;
    }
    state = AsyncValue.loading();
    await Future.delayed(Duration(seconds: 1));
    state = AsyncValue.data(currentValue + 1);
  }
}

スニペットでの記述

Flutter Riverpod Snippetsのキーワード「riverpodAsyncClass」で雛形を生成できます。クラス名や型パラメータ、buildメソッドは未入力になっているので、追記します。状態更新用のメソッドも合わせて追記します。

雛形

class  extends _$ {
  
  FutureOr<> build() {
    return ;
  }
}

プロバイダー定義とウィジェットでの利用例

同じファイル内で、プロバイダーとそれを利用するウィジェットを記述したものを掲載します。

コードのコメントはAIでつけました。

/use_generator/future/class_future_counter.dart
// FlutterのUIコンポーネントをインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を使用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Riverpodのコード生成アノテーションを使用するためにインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';

// コード生成されたファイル(`class_future_counter.g.dart`)をインポートします。このファイルは`@riverpod`アノテーションに基づいて自動生成されます。
part 'class_future_counter.g.dart';

// `FutureCounterNotifier`クラスをRiverpodのプロバイダとして定義することを示すアノテーションです。
// このクラスプロバイダは非同期的に値を管理します。

// `FutureCounterNotifier`は、コード生成によって`_`が接頭辞として付いた`_$FutureCounterNotifier`を継承します。
// これにより、`AsyncNotifier`の機能が提供されます。
class FutureCounterNotifier extends _$FutureCounterNotifier {
  // プロバイダの初期状態を非同期的に定義するメソッドです。
  // この場合、初期値を取得するまでに3秒かかります。
  
  Future<int> build() async {
    // 3秒間の遅延をシミュレートします。これは非同期処理(例: ネットワーク通信)を模倣しています。
    await Future.delayed(Duration(seconds: 3));
    // 非同期処理が成功した場合に返す初期値です。
    return 0;
  }

  // カウンターの値を1増やすメソッドです。この処理も非同期的に行われます。
  void increment() async {
    // 現在のプロバイダの状態から値(int型)を安全に取得します。
    // もし現在の状態がデータを持たない(ローディング中やエラー)場合は`null`を返します。
    final currentValue = state.valueOrNull;
    // `currentValue`が`null`の場合(例:まだ初期ロードが完了していない、またはエラー状態)は、処理を中断します。
    if (currentValue == null) {
      return;
    }
    // カウントアップ処理が始まったことを示すため、プロバイダの状態を「ロード中」に設定します。
    // これにより、`watch`しているウィジェット側がロード中UIに切り替わります。
    state = AsyncValue.loading();
    // カウントアップ処理にかかる時間をシミュレートするために1秒間遅延します。
    await Future.delayed(Duration(seconds: 1));
    // 新しいカウント値(現在の値に1を加えたもの)でプロバイダの状態を更新します。
    // `AsyncValue.data()`を使って、データが利用可能になったことを示します。
    state = AsyncValue.data(currentValue + 1);
  }
}

// Riverpodの非同期プロバイダを読み取るためのウィジェットを定義します。
// `ConsumerWidget`は、`WidgetRef`にアクセスし、プロバイダの状態変化に反応するために使用されます。
class FutureCounter extends ConsumerWidget {
  const FutureCounter({super.key});

  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef`を使用してプロバイダにアクセスします。
  Widget build(BuildContext context, WidgetRef ref) {
    // `futureCounterNotifierProvider`を監視(watch)し、その値を`AsyncValue<int>`型で取得します。
    // `watch`は、プロバイダの値(ローディング、データ、エラー)が変更されるたびにこのウィジェットを再構築します。
    final AsyncValue<int> counter = ref.watch(futureCounterNotifierProvider);

    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text('(geneあり)クラス Future:'),
        // `AsyncValue`の`when`メソッドを使って、非同期処理の現在の状態に応じて異なるUIを表示します。
        counter.when(
            // 非同期処理が「ロード中」の状態の場合に表示されるウィジェットです。
            loading: () => Text('ロード中'),
            // 非同期処理が成功し「データ」が取得できた状態の場合に表示されるウィジェットです。
            // 取得したカウンター値を文字列に変換して表示します。
            data: (counter) => Text('$counter'),
            // 非同期処理中に「エラー」が発生した場合に表示されるウィジェットです。
            // エラーオブジェクトを文字列に変換して表示します。
            error: (e, st) => Text(e.toString())),
        SizedBox(
          width: 16,
        ),
        ElevatedButton(
            onPressed: () {
              // `futureCounterNotifierProvider`の`notifier`(`FutureCounterNotifier`のインスタンス)を取得し、
              // その`increment`メソッドを呼び出してカウンターの値を増やします。
              // `read`は一度だけプロバイダのインスタンスを取得し、変更を監視しません。
              ref.read(futureCounterNotifierProvider.notifier).increment();
            },
            child: Text('+'))
      ],
    );
  }
}

複数の非同期プロバイダーを待ち合わせる

概要

  • 複数の非同期プロバイダーを待ち合わせて1つの状態として扱うプロバイダーを用意する
  • ウィジェット側では、結合された1つのプロバイダーを監視するだけで済む
  • .future プロパティで非同期処理の完了を待つ

実際のアプリ開発では、ユーザー情報と設定情報のように、画面表示に複数の非同期処理の結果が同時に必要になる場面があります。

複数の非同期プロバイダを「待ち合わせ」、それらの状態を一つにまとめて扱うための方法が用意されています。

これにより、すべてのデータが揃った時の状態、いずれかがローディング中の状態、一つでもエラーになった時の状態などを一箇所でシンプルに管理でき、UIのコードを簡潔に保つことができます。

書き方(.future プロパティで待ち合わせ)

複数のFutureProviderの結果をまとめて扱いたい場合、それらの完了を待つ新しいFutureProviderを定義するのが最もスマートな方法です。その鍵となるのが、.futureプロパティとawaitキーワードの組み合わせです。

通常、ref.watchで非同期プロバイダを監視すると、その状態はAsyncValueでラップされています。しかし、プロバイダの内部で別の非同期処理の完了をawaitで待ちたい場合、生のFutureオブジェクトが必要です。

そこで.futureプロパティを使います。await ref.watch(providerA.future)と記述すると、providerAが内部で実行しているFutureを直接取得し、その処理が完了するまで待機できます。

この方法の最大の利点は、状態管理が自動化される点です。待機中のproviderAがローディング中やエラー状態であれば、その状態は新しいプロバイダにも自動的に伝播し、UIに通知されます。

すべてのawaitが完了した後、得られた結果を組み合わせて最終的な値を返すことで、複数の非同期処理を安全に1つにまとめることができます。


Future<int> async1(Ref ref) async {
  await Future.delayed(Duration(seconds: 3));
  return 1;
}


Future<int> async2(Ref ref) async {
  await Future.delayed(Duration(seconds: 3));
  return 1;
}


Future<int> multiAsync(Ref ref) async {
  final int data1 = await ref.watch(async1Provider.future);
  final int data2 = await ref.watch(async2Provider.future);
  
  return data1 + data2;
}

プロバイダー定義とウィジェットでの利用例

同じファイル内で、プロバイダーとそれを利用するウィジェットを記述したものを掲載します。

コードのコメントはAIでつけました。

/use_generator/future/fn_future_multi_async.dart
// FlutterのUIコンポーネントをインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を使用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Riverpodのコード生成アノテーションを使用するためにインポートします
import 'package:riverpod_annotation/riverpod_annotation.dart';

// コード生成されたファイル(`fn_future_multi_async.g.dart`)をインポートします。このファイルは`@riverpod`アノテーションに基づいて自動生成されます。
part 'fn_future_multi_async.g.dart';

// `async1`関数をRiverpodのプロバイダとして定義することを示すアノテーションです。
// このプロバイダは非同期的に整数値を返します。

// `async1`という名前のプロバイダを定義します。
// このプロバイダは`Future<int>`を返し、非同期処理を伴います。
Future<int> async1(Ref ref) async {
  // 3秒間の遅延をシミュレートします。これは非同期処理(例: ネットワーク通信)を模倣しています。
  await Future.delayed(Duration(seconds: 3));
  // 非同期処理が成功した場合に返す整数値です。
  return 1;
}

// `async2`関数をRiverpodのプロバイダとして定義することを示すアノテーションです。
// このプロバイダは非同期的に整数値を返します。

// `async2`という名前のプロバイダを定義します。
// このプロバイダは`Future<int>`を返し、非同期処理を伴います。
Future<int> async2(Ref ref) async {
  // 3秒間の遅延をシミュレートします。これは非同期処理(例: ネットワーク通信)を模倣しています。
  await Future.delayed(Duration(seconds: 3));
  // 非同期処理が成功した場合に返す整数値です。
  return 1;
}

// `multiAsync`関数をRiverpodのプロバイダとして定義することを示すアノテーションです。
// このプロバイダは複数の非同期プロバイダの結果を結合します。

// `multiAsync`という名前のプロバイダを定義します。
// このプロバイダは`Future<int>`を返し、非同期処理を伴います。
Future<int> multiAsync(Ref ref) async {
  // `async1Provider`のFutureを監視し、その解決を待ちます。
  // `await`により、`async1Provider`がローディング中またはエラーの場合、`multiAsync`プロバイダもその状態に自動的に遷移します。
  final int data1 = await ref.watch(async1Provider.future);
  // `async2Provider`のFutureを監視し、その解決を待ちます。
  // `await`により、`async2Provider`がローディング中またはエラーの場合、`multiAsync`プロバイダもその状態に自動的に遷移します。
  final int data2 = await ref.watch(async2Provider.future);
  
  // 2つの非同期処理の結果を合計して返します。
  return data1 + data2;
}

// Riverpodの非同期プロバイダを読み取るためのウィジェットを定義します。
// `ConsumerWidget`は、`WidgetRef`にアクセスし、プロバイダの状態変化に反応するために使用されます。
class FutureMutltiAsync extends ConsumerWidget {
  const FutureMutltiAsync({super.key});

  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef`を使用してプロバイダにアクセスします。
  Widget build(BuildContext context, WidgetRef ref) {
    // `multiAsyncProvider`を監視(watch)し、その値を`AsyncValue<int>`型で取得します。
    // `watch`は、プロバイダの値(ローディング、データ、エラー)が変更されるたびにこのウィジェットを再構築します。
    final AsyncValue<int> multiAsync = ref.watch(multiAsyncProvider);

    return Row(
      mainAxisAlignment: MainAxisAlignment.start,
      children: [
        Text('(geneあり)関数 Future 複数:'),
        // `AsyncValue`の`when`メソッドを使って、非同期処理の現在の状態に応じて異なるUIを表示します。
        multiAsync.when(
            // 非同期処理が「ロード中」の状態の場合に表示されるウィジェットです。
            loading: () => Text('ロード中'),
            // 非同期処理が成功し「データ」が取得できた状態の場合に表示されるウィジェットです。
            // 取得した合計値を文字列に変換して表示します。
            data: (sum) => Text('$sum'),
            // 非同期処理中に「エラー」が発生した場合に表示されるウィジェットです。
            // エラーオブジェクトを文字列に変換して表示します。
            error: (e, st) => Text(e.toString())),
      ],
    );
  }
}
参考リンク集

【コード生成なし】Provider


コード生成を使わずにプロバイダーを定義する例を掲載します。

以下はコード生成の関数プロバイダーに相当します。

/no_generator/provider_hello.dart
// FlutterのUIウィジェット(この場合はCupertinoスタイル)をインポートします
import 'package:flutter/cupertino.dart';
// Riverpodの状態管理機能を利用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 'Hello Flutter'という文字列を提供する、最もシンプルなProviderを定義します
final helloProvider = Provider((ref) {
  // このプロバイダが提供する値を返します
  return 'Hello Flutter';
});

// Providerの値を読み取るためのウィジェットを定義します
// ConsumerWidgetを継承することで、buildメソッド内で`WidgetRef`が使えるようになります
class Hello extends ConsumerWidget {
  // このウィジェットのコンストラクタを定義します
  const Hello({super.key});

  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef` ref を使ってプロバイダにアクセスします
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watchを使い、helloProviderの現在の値('Hello Flutter')を取得します
    final hello = ref.watch(helloProvider);
    
    // 取得した値を画面に表示するUIを構築します
    return Row(
      // 子ウィジェットを左端から配置します
      mainAxisAlignment: MainAxisAlignment.start,
      // Row内に表示するウィジェットのリストです
      children: [
        // 固定のラベルを表示するテキストウィジェットです
        Text('(geneなし)関数 Provider:'),
        // プロバイダから取得した値を表示するテキストウィジェットです
        Text(hello),
      ],
    );
  }
}

【コード生成なし】NotifierProvider

コード生成を使わずにプロバイダーを定義する例を掲載します。

以下はコード生成のクラスプロバイダーに相当します。

lib/no_generator/notifierprovider_counter.dart
// FlutterのUIウィジェット(この場合はMaterial Design)をインポートします
import 'package:flutter/material.dart';
// Riverpodの状態管理機能を利用するためにインポートします
import 'package:flutter_riverpod/flutter_riverpod.dart';

// 状態(この場合はint型)とその変更ロジックを管理するNotifierクラスを定義します
class CounterNotifier extends Notifier<int> {
  
  // このNotifierが初めて読み込まれたときに状態を初期化するメソッドです
  build() {
    // カウンターの初期値を0に設定します
    return 0;
  }

  // 状態を更新するためのメソッドを定義します
  void increment() {
    // 現在の状態(state)に1を足して、新しい状態として更新します
    state = state + 1;
  }
}

// 上で定義したCounterNotifierのインスタンスを提供するNotifierProviderを定義します
final counterNotifierProvider = NotifierProvider<CounterNotifier, int>(() {
  // このプロバイダが要求されたときに、CounterNotifierの新しいインスタンスを返します
  return CounterNotifier();
});

// Providerの値を読み取り、UIを構築するウィジェットを定義します
class Counter extends ConsumerWidget {
  // このウィジェットのコンストラクタを定義します
  const Counter({super.key});

  
  // ウィジェットのUIを構築するメソッドです。`WidgetRef` ref を使ってプロバイダにアクセスします
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.watchを使い、counterNotifierProviderの現在の状態(int型のカウンター値)を取得します
    final counter = ref.watch(counterNotifierProvider);

    // 取得した値を画面に表示するUIを構築します
    return Row(
      // 子ウィジェットを左端から配置します
      mainAxisAlignment: MainAxisAlignment.start,
      // Row内に表示するウィジェットのリストです
      children: [
        // 固定のラベルを表示するテキストウィジェットです
        Text('(geneなし)クラス NotifierProvider:'),
        // プロバイダから取得した現在のカウンター値を表示します
        Text('$counter'),
        // ウィジェット間に16ピクセルの間隔を設けます
        SizedBox(
          width: 16,
        ),
        // ボタンウィジェットを定義します
        ElevatedButton(
            // ボタンが押されたときの処理を定義します
            onPressed: () {
              // ref.readを使い、CounterNotifierのインスタンスを取得し、incrementメソッドを呼び出します
              // .notifierを付けることで、状態そのものではなく、Notifierクラスのインスタンスにアクセスできます
              ref.read(counterNotifierProvider.notifier).increment();
            },
            // ボタン内に表示するテキストです
            child: Text('+'))
      ],
    );
  }
}

プロバイダーの種類まとめ2.0

関数プロバイダーは値の提供、クラスプロバイダーは変更可能な状態です。この中でさらに非同期やパラメータの有無などで分かれています。

参考リンク集

おわりに


本記事では、シンプルな値を提供する関数プロバイダーから、状態を持つクラスプロバイダー、さらには引数や非同期処理を扱う応用的なパターンまで、Riverpodコード生成の基本的な書き方をひと通りご紹介しました。

最初は見慣れない@riverpod_$クラス名といった記述も、それぞれの役割とパターンを理解すれば、コード生成がいかに強力で、安全かつ効率的な状態管理を実現してくれるかを感じていただけたかと思います。

皆さんのRiverpodに対する漠然とした不安を解消し、日々の開発で自信を持ってプロバイダーを使い分けるための一助となれば幸いです。

Discussion