[読書会]Riverpod.dev/docs 翻訳及び補足(Essentials)

Riverpod.dev/docs(開始URL: https://riverpod.dev/docs/introduction/why_riverpod )には日本語訳ページ(/ja)も部分的には既にあるのですが、その部分はそのまま参照して、その他の英語訳のままのページについては自力で翻訳および解説を補足してみました。まだ工事中ですが最終的に満足ができたら上記サイトの翻訳ページに献上できないかな等という目標もあります。

本ブログの(上記Riverpod.dev/docsの)翻訳および解説の目次頁(構成上のトップページ)は次のURLになります。

本頁は、上記のうちの、「Essentials」の章の翻訳および解説頁になります。

目次 index【閲覧時間3分】 click !!>>

Essentials

注釈)現在Riverpod.dev/docs中の本章「Essentials」には既に日本語訳ページ(https://riverpod.dev/ja/docs/essentials/first_request)が存在しますので、原文はこちらを参照願います。当ブログではこの日本語訳はそのまま参照して補足を追加しています。

2.本編:旧Ver習得済の方向け

Make your first provider/network request

最初のプロバイダー/ネットワーク リクエストを行う
(既翻訳サイト:https://riverpod.dev/ja/docs/essentials/first_request

ネットワークリクエストは、アプリケーションのコアとなる部分です。 しかし、ネットワークリクエストを行う際には考慮すべき点がたくさんあります:

  • リクエストが行われる間、UI はローディング状態をレンダリングする必要があります。
  • エラーは正しく処理されるべきです。
  • リクエストは可能な限りキャッシュされるべきです。

このセクションは、Riverpod がこれらの問題をどのように解決するのかを説明します。

ProviderScope の設定

ネットワークリクエストを開始する前に、アプリケーションのルートProviderScope 追加されていることを確認してください。

Dart
void main() {
  runApp(
    //Riverpodをインストールするには、このウィジェットを他のすべてのウィジェットの上に追加する必要があります。
    //このウィジェットは "MyApp" 内ではなく、"runApp" のパラメータとして直接追加する必要があります。
    ProviderScope(
      child: MyApp(),
    ),
  );
}

これにより、アプリケーション全体で Riverpod が有効になります。

“provider”でネットワークリクエストを実行する

ネットワークリクエストの実行は通常”ビジネスロジック“と呼ばれます。 Riverpod では、ビジネスロジックは”provider”の内部に配置されます。 provider は非常に強力な関数です。 通常の関数のように動作しますが、以下の利点があります:

  • キャッシュされる。
  • デフォルトのエラー/ローディング処理を提供する。
  • リスニング可能。
  • データが変更されたときに自動的に再実行される。

これにより、プロバイダーはGETネットワークリクエストに最適です(POST/etcリクエストについては、副次的効果をご覧ください)。

例として、退屈なときにランダムなアクティビティを提案するシンプルなアプリケーションを作成しましょう。 これを行うために、Bored API を使用します。
具体的には、/api/activityエンドポイントでGETリクエストを実行します。
これにより JSON オブジェクトが返され、それを Dart クラスインスタンスにパースします。
次のステップでは、このアクティビティを UI に表示します。
リクエスト中にローディング状態を表示し、エラーを優雅に処理することも確認します。

どうですか?では始めましょう!

モデルの定義

始める前に、API から受信するデータのモデルを定義する必要があります。
このモデルは、API のレスポンスを Dart クラスインスタンスに変換するために使用されます。

一般的に、JSON デコードを処理する際には、Freezed json_serializable などのコード生成パッケージを使用することをおすすめします。もちろん、手動で処理することも可能です。
どちらにせよ、最終的には次のようなモデルになります:

Dart
import 'package:freezed_annotation/freezed_annotation.dart';

part 'activity.freezed.dart';
part 'activity.g.dart';

///`GET /api/activity`エンドポイントのレスポンス
///`freezed``json_serializable`を使って定義されています。
@freezed
class Activity with _$Activity {
  factory Activity({
    required String key,
    required String activity,
    required String type,
    required int participants,
    required double price,
  }) = _Activity;

  ///JSONオブジェクトを[Activity]インスタンスに変換することで、
  ///APIレスポンスの型安全な読み取りが可能になります。
  factory Activity.fromJson(Map<String, dynamic> json) => _$ActivityFromJson(json);
}

provider の作成

モデルができたので、API クエリを開始できます。 これを行うには、最初の provider を作成する必要があります。
provider を定義する構文は次のようになります:

Dart
@riverpod
Result myFunction(MyFunctionRef ref) {
  <your logic here>
}
annotation すべての provider には @riverpod または @Riverpod()の annotation が必要です。
この annotation はグローバル関数またはクラスに付けることができます。
この annotation を通じて provider を構成することができます。
たとえば, @Riverpod(keepAlive: true)と書くことで “auto-dispose”を無効にすることができます(後で説明します)。
annotationed関数 annotationed関数は、
この annotation はprovider との対話方法を決定します。
例えばmyFunctionという関数の場合、 myFunctionProvider 変数が生成されます。

annotationed関数は、必ず最初のパラメーターにrefを指定する必要があります。
それに加えて、関数は任意の数のパラメーターを持つことができ、ジェネリクスも含めることができます。
annotationed関数はまた、Future/Streamを返すこともできます。

この関数は provider が最初に読み取られたときに呼び出されます。
その後の読み取りは関数を再度呼び出すのではなく、キャッシュされた値を返します。
Ref 他の provider と対話するために使用されるオブジェクトです。
この annotation はグローバル関数またはクラスに付けることができます。
すべての provider が持っています。provider 関数のパラメーターとして、または Notifier のプロパティとして存在します。
このオブジェクトのタイプは function/class の名前によって決定されます。

私たちの場合、API からアクティビティをGETしたいですよね。
GETは非同期操作であるため、Futureを作成する必要があります。

先に定義した構文を使用して、provider を次のように定義できます:

Dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'activity.dart';

//コード生成が機能するために必要。
part 'provider.g.dart';

//これにより、`activityProvider`という名前のproviderが作成され、
//この関数の結果をキャッシュする。
@riverpod
Future<Activity> activity(ActivityRef ref) async {
  //httpパッケージを使用して、Bored APIからランダムなアクティビティを取得する。
  final response
   = await http.get(Uri.https('boredapi.com', '/api/activity'));
  //dart:convertパッケージを使用し、JSONペイロードをMapデータ構造にデコードする。
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  // 最終的にMapをActivityインスタンスに変換する。
  return Activity.fromJson(json);
}

このスニペットでは、UI がランダムなアクティビティを取得するために使用できるactivityProviderという provider を定義しました。 ここで注目すべき点は:

  • ネットワークリクエストは、UI が provider を少なくとも一度読み取るまで実行されません。
  • その後の読み取りは、ネットワークリクエストを再実行せず、前回取得したアクティビティを返します。
  • UI がこの provider の使用を停止すると、キャッシュは破棄されます。
    その後、UI が再度 provider を使用すると、新しいネットワークリクエストが実行されます。
  • エラーをキャッチしませんでした。これは意図的で、provider は自然にエラーを処理します。
    ネットワークリクエストや JSON 解析でエラーが発生した場合、エラーは Riverpod によってキャッチされます。 その後、UI は自動的にエラーページを表示するための必要な情報を持つことになります。

備考)
provider は”lazy”なので”遅延評価”です。provider を定義することは、ネットワークリクエストを実行することではありません。
provider が最初に読み取られたときにネットワークリクエストが実行されます。

ネットワークリクエストのレスポンスを UI に表示する

provider を定義したので、UI 内でこれを使用してアクティビティを表示することができます。

provider と対話するためには、”ref”と呼ばれるオブジェクトが必要です。
これは、provider の定義で見たように、provider は自然に”ref”オブジェクトにアクセスできます。
しかし、私たちは provider 内ではなく、ウィジェットにいます。では、どうやって”ref”を取得するのでしょうか?

解決策は、Consumerというカスタムウィジェットを使用することです。 ConsumerはBuilderに似たウィジェットですが、”ref”を提供するという追加の利点があります。 これにより、UI が provider を読み取ることができます。 次の例はConsumerの使用方法を示しています:

Dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'activity.dart';
import 'provider.dart';

//アプリケーションのホームページ
class Home extends StatelessWidget {
  const Home({super.key});

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, ref, child) {
        //activityProviderを読み取ります。これにより、ネットワークリクエストが 
        //まだ開始されていない場合は開始されます。
        //ref.watchを使用することでWidgetはactivityProviderが更新される度に 
        //再構築されます。これは次の場合に発生する可能性があります::
        //-レスポンスが"AsyncLoading"から"AsyncData/AsyncError"に変わった時
        //-リクエストがリフレッシュされたとき
        //-結果がローカルで変更されたとき(副作用を実行した場合など)
        //...
        final AsyncValue<Activity> activity
         = ref.watch(activityProvider);

        return Center(
          //ネットワークリクエストは非同期であり、失敗する可能性があるため、 
          //エラー状態とローディング状態の両方を処理する必要があります。 
          //これにはパターンマッチングを使用できます。
          //または、
          //`if (activity.isLoading) { ... } else if (...)`
          //を使用することもできます。
          child: switch (activity) {
            AsyncData(:final value)
             => Text('Activity: ${value.activity}'),
            AsyncError()
             => const Text('Oops, something unexpected happened'),
            _ => const CircularProgressIndicator(),
          },
        );
      },
    );
  }
}

このスニペットでは、Consumerを使用してactivityProviderを読み取り、アクティビティを表示しました。 また、ローディング/エラー状態も優雅に処理しました。 provider 内で特別なことをすることなく、UI がローディング/エラー状態を処理できることに注目してください。 同時に、ウィジェットが再構築された場合、ネットワークリクエストは正しく再実行されません。 他のウィジェットも同じ provider にアクセスしてネットワークリクエストを再実行しないようにできます。

備考)
ウィジェットは好きなように多くの provider をリッスンできます。これを行うには、単にref.watch呼び出しを追加してください。

ヒント)
リンターをインストールすることを忘れないでください。 これにより、IDE がリファクタリングオプションを提供し、自動的に Consumer を追加したり、StatelessWidget を ConsumerWidget に変換したりすることができます。
インストール手順については、riverpod_lint/custom_lint の有効化をご覧ください。

さらに進む: Consumer の代わりに ConsumerWidgetを使用してコードのインデントを削減する

前の例では、provider を読み取るためにConsumerを使用しました。 このアプローチに問題はありませんが、追加のインデントがコードの読みやすさを損なう可能性があります。

Riverpod は、同じ結果を達成するための別の方法を提供します: StatelessWidget/StatefulWidgetがConsumerを返す代わりに、ConsumerWidget/ConsumerStatefulWidgetを定義できます。 ConsumerWidgetと ConsumerStatefulWidgetは、StatelessWidget/StatefulWidgetとConsumerの融合です。 これらは元のカウンターパートと同じように動作しますが、”ref”を提供するという利点があります。

前の例を次のように書き換えることができます:

Dart
//"StatelessWidget"の代わりに"ConsumerWidget"をサブクラス化しました。
//これは"StatelessWidget"を作成し、"Consumer"を返すのと同等です。
class Home extends ConsumerWidget {
  const Home({super.key});

  @override
  //"build"メソッドが追加パラメータ"ref"を受け取ることに注意してください。
  Widget build(BuildContext context, WidgetRef ref) {
    //"Consumer"を使用していた様に、ウィジェット内で"ref.watch"を使用できます。
    final AsyncValue<Activity> activity = ref.watch(activityProvider);

    //レンダリングロジックはそのままです
    return Center(/* ... */);
  }
}

ConsumerStatefulWidgetの場合は、次のように書きます:

Dart
//ConsumerStatefulWidgetを拡張します。
//これは"Consumer"+"StatefulWidget"と同等です。
class Home extends ConsumerStatefulWidget {
  const Home({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _HomeState();
}

//"State"の代わりに"ConsumerState"を拡張していることに注意してください。
//これは"ConsumerWidget"と"StatelessWidget"の関係と同じ原理を使用しています。
class _HomeState extends ConsumerState<Home> {
  @override
  void initState() {
    super.initState();

    //状態ライフサイクルも"ref"にアクセスできます。
    //これにより、特定のproviderにリスナーを追加して
    //ダイアログやスナックバーを表示することができます。
    ref.listenManual(activityProvider, (previous, next) {
      //TODO snackbar/dialogを表示する。
    });
  }

  @override
  Widget build(BuildContext context) {
    //"ref"はパラメータとして渡されず、
    //"ConsumerState"のプロパティに含まれている。
    //したがって、"build"内で引き続き"ref.watch"を使用できます。
    final AsyncValue<Activity> activity = ref.watch(activityProvider);

    return Center(/* ... */);
  }
}

FlutFlutter_hooks の考慮事項: HookWidgetと ConsumerWidgetの組み合わせ

注意)

“hooks”について聞いたことがない場合は、このセクションをスキップしてください。
Flutter_hooksは Riverpod とは独立したパッケージですが、よく一緒に使用されます。
Riverpod を初めて使用する場合、”hooks”を使用することはお勧めしません。詳細はabout_hooksをご覧ください。

flutter_hooksを使用している場合、 HookWidgetと ConsumerWidgetをどのように組み合わせるかを疑問に思うかもしれません。
結局のところ、どちらも拡張するウィジェットクラスを変更することが関わります。

Riverpod はこの問題への解決策を提供します:
HookConsumerWidgetと StatefulHookConsumerWidgetです。
ConsumerWidgetと ConsumerStatefulWidgetが Consumerと StatelessWidget/StatefulWidgetの融合であるように、
HookConsumerWidgetと StatefulHookConsumerWidgetは Consumerと HookWidget/HookStatefulWidgetの融合です。
これにより、同じウィジェット内で hooks と provider の両方を使用できるようになります。

これを示すために、前の例をもう一度書き直すことができます:

Dart
//"HookConsumerWidget"をサブクラス化しました。  
//これにより 
//"StatelessWidget" + "Consumer" + "HookWidget"
//が組み合わさります。
class Home extends HookConsumerWidget {
  const Home({super.key});

  @override
  //"build"メソッドに"ref"パラメータが追加されたことに注目してください。 
  Widget build(BuildContext context, WidgetRef ref) {
    //ウィジェット内で"useState"などのhooksを使用することができます。
    final counter = useState(0);

    //providerプロバイダーも読み取ることができます。
    final AsyncValue<Activity> activity = ref.watch(activityProvider);

    return Center(/* ... */);
  }
}

副作用の実行

これまで、データの取得方法(いわゆるGET リクエストの実行)についてのみ見てきました。
しかし、POST リクエストのような副作用はどうでしょうか?

アプリケーションはしばしば CRUD(作成、読み取り、更新、削除)API を実装します。
その際、更新リクエスト(通常はPOST)はローカルキャッシュも更新して UI に新しい状態を反映させることが一般的です。

問題は、Consumer の中から provider の状態をどう更新するかです。
当然、provider は状態を変更する方法を公開していません。
これは、状態が制御された方法でのみ変更されるようにし、関心の分離を促進するための設計です。
代わりに、provider は状態を変更する方法を明示的に公開する必要があります。

そのために、新しい概念を使用します: Notifiers.
この新しい概念を紹介するために、もう少し進んだ例を使用します:ToDo リスト

Notifier の定義

ここで、これまでに知っていることから始めましょう:シンプルなGET リクエストです。 最初の provider/ネットワークリクエストを作成するで見たように、次のように書いて ToDo リストを取得できます:

Dart
@riverpod
Future<List<Todo>> todoList(TodoListRef ref) async {
  //ネットワーク要求をシミュレートします。これは通常、実際のAPIから来るものです。
  return [
    Todo(description: 'Learn Flutter', completed: true),
    Todo(description: 'Learn Riverpod'),
  ];
}

今度は、取得した ToDo リストに新しい ToDo を追加する方法を見てみましょう。 これには、provider が状態を変更するための公開 API を提供するように変更する必要があります。
これを行うために、provider を”notifier”に変換します。

Notifiers は Provider の”stateful widget”です。
provider を定義するための構文に少し修正が必要です。
この新しい構文は次のとおりです:

Dart
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  Result build() {
    <your logic here>
  }
  <your methods here>
}
annotation すべての Provider は@riverpodまたは@Riverpod()でアノテーションする必要があります。
このアノテーションはグローバル関数またはクラスに配置できます。
このアノテーションを通して、provider を設定できます。

例えば、 @Riverpod(keepAlive: true)と書くことで”auto-dispose”(後で説明)を無効にすることができます。
Notifier クラスに@riverpod アノテーションが付けられるとそのクラスは “Notifier”と呼ばれます。
そのクラスは _$NotifierNameを拡張する必要があり、 NotifierNameはクラス名です。

Notifiers は、provider の状態を変更する方法を公開する責任を負います。
このクラスのパブリックメソッドには、ref.read(yourProvider.notifier).yourMethod()を使用して consumers がアクセスできます。 (NOTE)UI で状態が変更されたことを知る手段がないため、Notifiers には組み込みの state 以外の公開プロパティがあってはなりません。
buildメソッド 全ての notifiers は build メソッドをオーバーライドする必要があります。
このメソッドは、通常、non-notifier provider でロジックを入れる場所に相当します。

このメソッドは直接呼び出してはいけません。

参考までに、この新しい構文を以前見た構文と比較するために最初の provider/ネットワークリクエストを作成する を確認すると良いでしょう。

備考)
build以外のメソッドがない Notifier は、以前見た構文を使用するのと同じです。

最初の provider/ネットワークリクエストを作成する に示された構文は、UI から変更する方法がない notifiers に対する省略形と考えることができます。

構文を見たところで、以前定義した Provider を Notifier に変換する方法を見てみましょう:

Dart
@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<Todo>> build() async {
    // 以前FutureProviderに記述していたロジックがbuildメソッドにあります。
    return [
      Todo(description: 'Learn Flutter', completed: true),
      Todo(description: 'Learn Riverpod'),
    ];
  }
}

ウィジェット内で provider を読み取る方法は変更されていません。
以前の構文と同様に ref.watch(todoListProvider) を使用できます。

注意)
notifier のコンストラクタにロジックを入れないでください。
ref およびその他のプロパティはその時点ではまだ使用できないため、Notifiers にはコンストラクタがないはずです。
代わりに、ロジックをbuildメソッドに入れてください。

Dart
class MyNotifier extends ... {
  MyNotifier() {
    // ❌ これはしないでください。
    // 例外を投げます。
    state = AsyncValue.data(42);
  }

  @override
  Result build() {
    // ✅ 代わりにここにロジックを入れてください。
    state = AsyncValue.data(42);
  }
}

POSTリクエストを実行するメソッドの公開

Notifier ができたので、今度は副作用を実行するメソッドを追加できます。
その副作用の一つとして、新しい ToDo をPOSTするクライアントを作成することがあります。
notifier にaddTodoメソッドを追加することでそれが可能になります:

Dart
@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<Todo>> build() async => [/* ... */];

  Future<void> addTodo(Todo todo) async {
    await http.post(
      Uri.https('your_api.com', '/todos'),
      // We serialize our Todo object and POST it to the server.
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(todo.toJson()),
    );
  }
}

次に、最初の provider/ネットワークリクエストを作成する で見たのと同じConsumer/ConsumerWidgetを使用して UI でこのメソッドを呼び出せます:

Dart
class Example extends ConsumerWidget {
  const Example({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () {
        // Using "ref.read" combined with "myProvider.notifier", we can
        // obtain the class instance of our notifier. This enables us
        // to call the "addTodo" method.
        ref
            .read(todoListProvider.notifier)
            .addTodo(Todo(description: 'This is a new todo'));
      },
      child: const Text('Add todo'),
    );
  }
}

備考)
メソッドを呼び出す際にref.watch の代わりに ref.readを使用していることに注意してください。
技術的にはref.watchでも動作しますが、
“onPressed”のようなイベントハンドラーでロジックを実行する場合は、ref.readを使用することをお勧めします。

これで、ボタンを押すとPOSTリクエストを行うボタンができました。
しかし、現時点では、UI が新しい ToDo リストを反映して更新されることはありません。
ローカルキャッシュをサーバーの状態と一致させたいと思います。

これにはいくつかの方法があり、それぞれに利点と欠点があります。

API レスポンスに合わせてローカルキャッシュを更新する

一般的なバックエンドの慣行は、POSTリクエストがリソースの新しい状態を返すようにすることです。
特に、API は新しい Todo を追加したリストを返します。 これを行う方法の一つはstate = AsyncData(response)と記述することです:

Dart
  Future<void> addTodo(Todo todo) async {
    //POSTリクエストは、新しいアプリケーションの状態と一致するList<Todo>を返します。
    final response = await http.post(
      Uri.https('your_api.com', '/todos'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(todo.toJson()),
    );

    //API応答をデコードしてList<Todo>に変換します。
    List<Todo> newTodos = (jsonDecode(response.body) as List)
        .cast<Map<String, Object?>>()
        .map(Todo.fromJson)
        .toList();

    //新しい状態と一致するようにローカルキャッシュを更新します。
    //これにより、すべてのリスナーに通知が送信されます。
    state = AsyncData(newTodos);
  }

利点)

  • UI は可能な限り最新の状態に保たれます。
    他のユーザーが ToDo を追加すると、私たちもそれを見ることができます。
  • サーバーが真実の源です。
    このアプローチを使用すると、クライアントは ToDo リストに新しい ToDo をどこに挿入するかを知る必要がありません。
  • ネットワークリクエストは一度だけ必要です。

欠点)

  • このアプローチは、サーバーが特定の方法で実装されている場合にのみ機能します。
    サーバーが新しい状態を返さない場合、このアプローチは機能しません。
  • フィルタリング/ソートなどが含まれる場合、またはより複雑なGETリクエストの場合、このアプローチは機能しない可能性があります。

ref.invalidateSelf()を使用して provider を更新する

もう一つの方法は、provider がGETリクエストを再実行するようにすることです。
これは、POSTリクエスト後にref.invalidateSelf()を呼び出すことで行います:

Dart
  Future<void> addTodo(Todo todo) async {
    //APIのレスポンスは気にしません。
    await http.post(
      Uri.https('your_api.com', '/todos'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(todo.toJson()),
    );

    //Postリクエストが完了すると、ローカルキャッシュをダーティー(Dirty)と表現することができます。
    //そうすると、notifierの"build"が非同期で再度呼び出されます、
    //この時、リスナーに通知が送信されます。
    ref.invalidateSelf();

    //(オプション)その後、新しいステータスが計算されるまで待つことができます。
    //これにより、新しい状態が利用可能になるまで、"addTodo "は完了しない。
    await future;
  }

利点)

  • UI は可能な限り最新の状態に保たれます。
    他のユーザーが ToDo を追加すると、私たちもそれを見ることができます。
  • サーバーが真実の源です。
    このアプローチを使用すると、クライアントは ToDo リストに新しい ToDo をどこに挿入するかを知る必要がありません。
  • このアプローチは、サーバーの実装に関係なく機能します。
    フィルタリング/ソートなどが含まれる場合、またはより複雑なGETリクエストの場合、特に有用です。

欠点)

  • このアプローチは追加のGETリクエストを実行するため、効率が悪い可能性があります。

ローカルキャッシュを手動で更新する

別の方法は、ローカルキャッシュを手動で更新することです。
これには、バックエンドの動作を模倣する作業が含まれます。
たとえば、バックエンドが新しい項目を先頭に挿入するのか末尾に挿入するのかを知る必要があります。

Dart
  Future<void> addTodo(Todo todo) async {
    // APIのレスポンスは重要ではありません。
    await http.post(
      Uri.https('your_api.com', '/todos'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode(todo.toJson()),
    );

    // その後、ローカルキャッシュを手動で更新することができます。 
    // そのためには、以前の状態を取得する必要があります。
    // 注意: 前の状態がまだロード中またはエラー状態である可能性があります。
    // これを処理するエレガントな方法は、`this.state`ではなく、`this.state`の代わりに 
    // `this.future`を読み込んで読み込み状態を待たせたり
    // ステータスがエラー状態の場合、エラーをスローします。
    final previousState = await future;

    // その後、新しい状態オブジェクトを作成して状態を更新することができます。
    // すると、すべてのリスナーに通知が送信されます。
    state = AsyncData([...previousState, todo]);
  }

備考)

この例では immutable state を使用しています。
必要ではありませんが、immutable state を使用することをお勧めします。
詳細はwhy_immutabilityを参照してください。
代わりに、mutable state を使用する場合は、別の方法を使用できます:

Dart
    final previousState = await future;
    // 以前のToDoリストを変更します。
    previousState.add(todo);
    // リスナーに手動で通知を送信します。
    ref.notifyListeners();

利点)

  • このアプローチはサーバーの実装に関係なく機能します。
  • ネットワークリクエストは 1 回だけ必要です。

欠点)

  • ローカルキャッシュはサーバーの状態と一致しない可能性があります。もし他のユーザーが ToDo を追加した場合、私たちはそれを見ることができません。
  • このアプローチはバックエンドのロジックを効果的に複製し、実装がより複雑になる可能性があります。

さらに進む:スピナーの表示とエラーハンドリング

これまで見てきたように、ボタンを押すとPOSTリクエストを行い、リクエストが完了すると UI が更新されて変更を反映します。
しかし、現時点では、リクエストが実行されていることを示すものや、失敗した場合の情報はありません。

一つの方法は、addTodo が返す Future をローカルウィジェットの状態に保存し、その Future をリッスンしてスピナーやエラーメッセージを表示することです。
このシナリオでは flutter_hooksが役立ちますが、
もちろん StatefulWidget を代わりに使用することもできます。

次のスニペットは、操作が保留中の間に進行状況インジケーターを表示し、 失敗した場合にボタンを赤く表示します:

Dart
class Example extends ConsumerStatefulWidget {
  const Example({super.key});

  @override
  ConsumerState<ConsumerStatefulWidget> createState() => _ExampleState();
}

class _ExampleState extends ConsumerState<Example> {
  // 保留中のaddTodo操作。保留中の操作がない場合は null。
  Future<void>? _pendingAddTodo;

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      // 保留中の操作をリッスンし、それに応じてUIを更新する。
      future: _pendingAddTodo,
      builder: (context, snapshot) {
        // エラー状態があるかどうかを計算します。
        // connectionStateチェックは、操作が再試行されたときに処理するため。
        final isErrored = snapshot.hasError && snapshot.connectionState != ConnectionState.waiting;

        return Row(
          children: [
            ElevatedButton(
              style: ButtonStyle(
                // エラーがある場合、ボタンが赤く表示されます。
                backgroundColor: WidgetStatePropertyAll(
                  isErrored ? Colors.red : null,
                ),
              ),
              onPressed: () {
                // addTodoが返したfutureを変数に保存します。
                final future = ref
                    .read(todoListProvider.notifier)
                    .addTodo(Todo(description: 'This is a new todo'));

                // そのfutureをローカルstateに保存します。
                setState(() {
                  _pendingAddTodo = future;
                });
              },
              child: const Text('Add todo'),
            ),
            // 作業が保留中です。インジケータを表示しています。
            if (snapshot.connectionState == ConnectionState.waiting) ...[
              const SizedBox(width: 8),
              const CircularProgressIndicator(),
            ]
          ],
        );
      },
    );
  }
}

リクエストに引数を渡す

前の記事では、シンプルなGET HTTP リクエストを行うための”provider”を定義する方法を見てきました。
しかし、HTTP リクエストは外部パラメータを必要とすることがよくあります。

例えば、以前はBored APIを使ってユーザーにランダムなアクティビティを提案しました。
しかし、ユーザーはアクティビティのタイプをフィルタリングしたいかもしれませんし、価格要件などもあるかもしれません。
これらのパラメータは事前にはわかりません。
そのため、これらのパラメータを UI から provider に渡す方法が必要です。

provider を引数を受け取るように更新する

以前、provider を次のように定義しました:

Dart
// "関数型" provider
@riverpod
Future<Activity> activity(ActivityRef ref) async {
  // TODO: アクティビティをフェッチするためにネットワークリクエストを実行する。
  return fetchActivity();
}

// または "notifier"
@riverpod
class ActivityNotifier2 extends _$ActivityNotifier2 {
  @override
  Future<Activity> build() async {
    // TODO: アクティビティをフェッチするためにネットワークリクエストを実行する。
    return fetchActivity();
  }
}

provider にパラメータを渡すには、アノテーションが付いた関数自体にパラメータを追加するだけです。
例えば、定義したいアクティビティの種類に対応する String 引数を受け取るように provider を更新することができます:

Dart
@riverpod
Future<Activity> activity(
  ActivityRef ref,
  // providerに引数を追加することができます。
  // パラメータの型は何でも構いません。
  String activityType,
) async {
  // 引数 "activityType "を使ってURLを作ることができる。
  // このポイントは "https://boredapi.com/api/activity?type=<activityType>"を指しています。
  final response = await http.get(
    Uri(
      scheme: 'https',
      host: 'boredapi.com',
      path: '/api/activity',
      // クエリパラメータを手動でエンコードする必要はなく、 "Uri" クラスが行ってくれます。
      queryParameters: {'type': activityType},
    ),
  );
  final json = jsonDecode(response.body) as Map<String, dynamic>;
  return Activity.fromJson(json);
}

@riverpod
class ActivityNotifier2 extends _$ActivityNotifier2 {
  /// Notifier 引数はbuildメソッドで指定します。
  /// 引数は好きなだけ増やすことができ、optional/名前付き引数にすることも可能です。
  @override
  Future<Activity> build(String activityType) async {
    // 引数は "this.<argumentName>" でも指定できます。
    print(this.activityType);

    // TODO: ネットワークリクエストを実行してアクティビティを取得します。
    return fetchActivity();
  }
}

注意)

provider に引数を渡すときは、provider に”autoDispose”を有効にすることを強くお勧めします。
これを怠るとメモリリークが発生する可能性があります。
詳細についてはauto_disposeを参照してください。

引数を渡すために UI を更新する

以前は、ウィジェットが次のように provider を消費(consumed)していました:

Dart
    AsyncValue<Activity> activity = ref.watch(activityProvider);

しかし、provider が引数を受け取るため、それを消費(consume)するための構文が少し異なります。
provider は今や要求されたパラメータを使用して呼び出す必要がある関数です。
UI を更新して、ハードコードされたアクティビティタイプを渡すことができます:

Dart
    AsyncValue<Activity> activity = ref.watch(
      // providerは、アクティビティタイプを期待する関数となります。
      // 今回は単純に、定数の文字列を渡しましょう。
      activityProvider('recreational'),
    );

provider に渡されるパラメータは、アノテーション付き関数のパラメータ(“ref” パラメータを除く)に対応します。

備考)

同じ provider に異なる引数で同時にリッスンすることは可能です。
例えば、UI は”recreational”と”cooking”の両方のアクティビティを表示することができます:

Dart
    return Consumer(
      builder: (context, ref, child) {
        final recreational = ref.watch(activityProvider('recreational'));
        final cooking = ref.watch(activityProvider('cooking'));

        // 両方のアクティビティを表示できます。
        // 両方のリクエストは並行して行われ、正しくキャッシュされる。
        return Column(
          children: [
            Text(recreational.valueOrNull?.activity ?? ''),
            Text(cooking.valueOrNull?.activity ?? ''),
          ],
        );
      },
    );
    

備考)

同じ provider に異なる引数で同時にリッスンすることは可能です。
例えば、UI は”recreational”と”cooking”の両方のアクティビティを表示することができます:

Dart
    return Consumer(
      builder: (context, ref, child) {
        final recreational = ref.watch(activityProvider('recreational'));
        final cooking = ref.watch(activityProvider('cooking'));

        // 両方のアクティビティを表示できます。
        // 両方のリクエストは並行して行われ、正しくキャッシュされる。
        return Column(
          children: [
            Text(recreational.valueOrNull?.activity ?? ''),
            Text(cooking.valueOrNull?.activity ?? ''),
          ],
        );
      },
    );    

キャッシングの考慮事項とパラメータの制限

provider にパラメータを渡す場合、計算は引き続きキャッシュされます。
違いは、計算がパラメータごとにキャッシュされることです。

これは、同じ provider を同じパラメータで消費(consume)する 2 つのウィジェットがある場合、ネットワークリクエストは 1 つだけ行われることを意味します。
しかし、異なるパラメータで同じ provider を消費する 2 つのウィジェットがある場合、2 つのネットワークリクエストが行われます。

これを実現するために、Riverpod はパラメータの == 演算子に依存しています。
したがって、provider に渡されるパラメータが一貫した等価性を持つことが重要です。

注意)

一般的なミスは、provider のパラメータとして == をオーバーライドしない新しいオブジェクトを直接インスタンス化することです。 例えば、次のように List を渡したくなるかもしれません:

Dart
    // 代わりに文字列リストを許可するようにactivityProviderを更新することができます。
    // その後、監視呼び出しから直接そのリストを作成することができます。
    ref.watch(activityProvider(['recreational', 'cooking']));

このコードの問題は、[‘recreational’, ‘cooking’] == [‘recreational’, ‘cooking’] が false であることです。
そのため、Riverpod は 2 つのパラメータが異なると見なし、新しいネットワークリクエストを試みます。
これは、無限ループのネットワークリクエストを引き起こし、ユーザーに進捗インジケーターが永久に表示される結果となります。

これを修正するには、const リスト (const [‘recreational’, ‘cooking’]) を使用するか、
== をオーバーライドするカスタムリスト実装を使用できます。

このミスを見つけやすくするために、 riverpod_lintを使用し、
provider_parameters lint ルールを有効にすることをお勧めします。
そうすると、前述のスニペットには警告が表示されます。
インストール手順については riverpod_lint/custom_lint の有効化 を参照してください。

Websocketsと同期実行

これまで Futureの作成方法についてのみ説明しました。
これは意図的なものであり、Future は Riverpod アプリケーションの基盤となるべきだからです。
しかし、必要に応じて Riverpod は他の形式もサポートしています。

特に、Futureの代わりに、provider は以下のことができます:

同期的にオブジェクトを返すこと(例:”Repository”の作成)。
Stream を返すこと(例:websockets の受信)。
Futureを返すことと、Streamやオブジェクトを返すことは全体的に非常に似ています。
このページでは、それらのケースにおける微妙な違いやさまざまなヒントを説明します。

同期的にオブジェクトを返すこと

オブジェクトを同期的に作成するには、provider が Future を返さないことを確認してください:

Dart
@riverpod
int synchronousExample(SynchronousExampleRef ref) {
  return 0;
}

provider が同期的にオブジェクトを作成する時、オブジェクトの消費(consumed)方法に影響を与えます。
特に、同期的な値は”AsyncValue”でラップされません:

Dart
  Consumer(
    builder: (context, ref, child) {
      // 値が "AsyncValue" でラップされていません。
      int value = ref.watch(synchronousExampleProvider);

      return Text('$value');
    },
  );

この違いの結果として、provider がエラーをスローした場合、値を読み取ろうとするとエラーが再スローされます。
代わりに、ref.listenを使用すると、“onError”コールバックが呼び出されます。

Listenable オブジェクトに関する考慮事項

ChangeNotifier や StateNotifierなどの Listenable オブジェクトはサポートされていません。
もし、互換性のためにこれらのオブジェクトのいずれかとやり取りする必要がある場合、その通知メカニズムを Riverpod にパイプ(pipe)することが 1 つの回避策です。

Dart
/// 値が変更されるたびにValueNotifierを生成してリスナーを更新するproviderです。
@riverpod
ValueNotifier<int> myListenable(MyListenableRef ref) {
  final notifier = ValueNotifier(0);

  // providerが破棄された時にnotifierを破棄します。
  ref.onDispose(notifier.dispose);

  // ValueNotifierが更新されるたびにproviderのリスナーに通知します。
  notifier.addListener(ref.notifyListeners);

  return notifier;
}

備考)

このようなロジックが何度も必要な場合、ロジックを共有する価値があります。
“ref”オブジェクトはコンポーザブルに設計されています。
これにより、provider の外で dispose/listening ロジックを抽出することができます:

Dart
extension on Ref {
  // 前述のロジックをRefの拡張に移動することができます。
  // これにより、provider間でロジックを再利用できるようになります。
  T disposeAndListenChangeNotifier<T extends ChangeNotifier>(T notifier) {
    onDispose(notifier.dispose);
    notifier.addListener(notifyListeners);
    // 使用を少し簡単にするために、notifierを返します。
    return notifier;
  }
}

@riverpod
ValueNotifier<int> myListenable(MyListenableRef ref) {
  return ref.disposeAndListenChangeNotifier(ValueNotifier(0));
}

@riverpod
ValueNotifier<int> anotherListenable(AnotherListenableRef ref) {
  return ref.disposeAndListenChangeNotifier(ValueNotifier(42));
}

Stream のリッスン

現代のアプリケーションの一般的なユースケースは、Firebase や GraphQL サブスクリプションなどの Websockets とやり取りすることです。
これらの API とやり取りする場合、多くの場合、Stream をリッスンします。

そのために、Riverpod は自然にStreamオブジェクトをサポートします。
Futureと同様に、オブジェクトは AsyncValue に変換されます。

Dart
@riverpod
Stream<int> streamExample(StreamExampleRef ref) async* {
  // 1秒ごとに0から41までの数字を返します。
  // これはFirestoreやGraphQL等のStreamに置き換えることが可能です。
  for (var i = 0; i < 42; i++) {
    yield i;
    await Future<void>.delayed(const Duration(seconds: 1));
  }
}

class Consumer extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // streamがリッスンされ、AsyncValueに変換されます。
    AsyncValue<int> value = ref.watch(streamExampleProvider);

    // AsyncValueを使ってロード/エラー状態を処理し、データを表示することができます。
    return switch (value) {
      AsyncValue(:final error?) => Text('Error: $error'),
      AsyncValue(:final valueOrNull?) => Text('$valueOrNull'),
      _ => const CircularProgressIndicator(),
    };
  }
}

備考)

Riverpod はRXのBehaviorSubjectなどのカスタム Stream 実装を認識していません。
そのため、BehaviorSubject を返しても、作成時に既に利用可能な場合でも、その value がウィジェットに同期的に公開されることはありません。

StreamやFutureのAsyncValueへの変換を無効にする

デフォルトでは、Riverpod はStreamやFutureをAsyncValueに変換します。
これを無効にすることができますが、これはまれに必要な場合のみです。
無効にするには、戻り値の型を Raw typedef でラップします。

注意)

AsyncValue変換を無効にすることは一般的には推奨されません。
これを行うのは、確実に必要である場合のみにしてください。

Dart
@riverpod
Raw<Stream<int>> rawStream(RawStreamRef ref) {
  // "Raw"はtypedefです。
  // 戻り値を"Raw"コンストラクタでラップする必要はありません。
  return const Stream<int>.empty();
}

class Consumer extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // 値はAsyncValueに変換されず、
    // 作成されたstreamはそのまま返されます。
    Stream<int> stream = ref.watch(rawStreamProvider);
    return StreamBuilder<int>(
      stream: stream,
      builder: (context, snapshot) {
        return Text('${snapshot.data}');
      },
    );
  }
}

リクエストの組み合わせ

これまで、リクエストが互いに独立している場合だけを見てきました。
しかし、一般的なユースケースとして、あるリクエストの結果に基づいて別のリクエストをトリガーする必要がある場合があります。

このため、リクエストに引数を渡すメカニズムを使用して、provider の結果を別の provider へのパラメータとして渡すことができます。

しかしこのアプローチはいくつか欠点があります:

  • 実装の詳細が漏れることになります。
    UI が他の provider によって使用されるすべての provider を知っている必要があります。
  • パラメータが変更されるたびに新しい state が作成されます。
    パラメータを渡すことで、パラメータが変更したときに前の state を維持する方法がありません。
  • リクエストの組み合わせが難しくなります。
  • 開発ツールは provider 間の関係を知らないため、ツールの有用性が低くなります。

これを向上させるため、Riverpod はリクエストの組み合わせに異なるアプローチを提供します。

基本: “ref” の取得

リクエストを組み合わせる全ての方法は 1 つの共通点があります:
それらはすべてRefオブジェクトに基づいています。

Ref オブジェクトは、すべての provider がアクセスできるオブジェクトです。
これにより、さまざまなライフサイクルリスナーにアクセスできるだけでなく、provider を組み合わせるさまざまなメソッドも提供します。

Ref を取得できる場所は、provider のタイプによって異なります。

機能的な provider では、Ref は provider の関数のパラメータとして渡されます:

Dart
@riverpod
int example(ExampleRef ref) {
  // "Ref "は、他のproviderを読むために使うことができます。
  final otherValue = ref.watch(otherProvider);

  return 0;
}

クラスの種類では、Ref は Notifier クラスのプロパティです:

Dart
@riverpod
class Example extends _$Example {
  @override
  int build() {
    //  "Ref "は、他のproviderを読むために使うことができます。
    final otherValue = ref.watch(otherProvider);

    return 0;
  }
}

ref を使用して provider を読む

ref.watch メソッド

Ref を取得したので、これを使用してリクエストを組み合わせることができます。
主な方法は ref.watch を使用することです。
一般的に、他のオプション以上にref.watch を使用することを推奨します,
これが一般的にメンテナンスを簡単にします。

ref.watch は provider を受け取り、最新の state を返します。
そして、監視された provider が変更されるたびに、provider は無効になり、次のフレームまたは次の読み取りで再構築されます。

ref.watchを使うことで、ロジックが”リアクティブ”かつ”宣言的”になります。
つまり、必要に応じてロジックが自動的に再計算されるようになります。
また、更新メカニズムが”on change”のような副作用に依存しないことを意味します。
これは StatelessWidgets が動作する方法と似ています。

例えば、ユーザーの位置情報を監視する provider を定義し、
その位置情報を使用してユーザーの近くのレストランのリストを取得することができます。

Dart
@riverpod
Stream<({double longitude, double latitude})> location(LocationRef ref) {
  // TO-DO: 現在位置を取得するstreamを返す
  return someStream;
}

@riverpod
Future<List<String>> restaurantsNearMe(RestaurantsNearMeRef ref) async {
  // 現在位置を取得するため"ref.watch"を使用する。
  // providerの後に".future "を指定することで、コードは少なくとも1つの場所が利用可能になるまで待ちます。
  final location = await ref.watch(locationProvider.future);

  // 現在位置に基づいてネットワークリクエストを作成することができました。
  // 例えばGoogle Map APIを使用することもできます。:
  // https://developers.google.com/maps/documentation/places/web-service/search-nearby
  final response = await http.get(
    Uri.https('maps.googleapis.com', 'maps/api/place/nearbysearch/json', {
      'location': '${location.latitude},${location.longitude}',
      'radius': '1500',
      'type': 'restaurant',
      'key': '<your api key>',
    }),
  );
  // JSONからレストラン名を取得する。
  final json = jsonDecode(response.body) as Map;
  final results = (json['results'] as List).cast<Map<Object?, Object?>>();
  return results.map((e) => e['name']! as String).toList();
}

備考)

監視された provider が変更され、リクエストが再計算されると、新しいリクエストが完了されるまで以前の状態が保持されます。
同時に、リクエストが保留中の間、”isLoading”と”isReloading”フラグがセットされます。

これにより、UI は前の状態や読み込みインジケーター、またはその両方を表示することができます。

備考)

ref.watch(locationProvider) ではなく、ref.watch(locationProvider.future) を使用したことに注目してください。
これは、locationProvider が非同期であるためです。そのため、初期値が利用可能になるのを待ちます。

もし .future を省略すると、locationProvider の現在の状態のスナップショットである AsyncValue を受け取ります。
しかし、まだ場所が利用できない場合は、何もできません。

注意)

“命令型”に実行されるコード内で ref.watch を呼び出すことは悪いプラクティスとされています。
これは、provider のビルド段階で実行されない可能性があるコードを意味します。
これには、”listener”コールバックや Notifier のメソッドが含まれます:

Dart
@riverpod
int example(ExampleRef ref) {
  ref.watch(otherProvider); // Good!
  ref.onDispose(() => ref.watch(otherProvider)); // Bad!

  final someListenable = ValueNotifier(0);
  someListenable.addListener(() {
    ref.watch(otherProvider); // Bad!
  });

  return 0;
}

@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  int build() {
    ref.watch(otherProvider); // Good!
    ref.onDispose(() => ref.watch(otherProvider)); // Bad!

    return 0;
  }

  void increment() {
    ref.watch(otherProvider); // Bad!
  }
}

ref.listen/listenSelf メソッド

ref.listen メソッドは ref.watchの代替手段です。
これは、従来の”listen”/”addListener”メソッドに似ています。
provider とコールバックを受け取り、provider の内容が変更されるたびにそのコールバックを呼び出します。

ref.listen から ref.watchにリファクタリングすることが一般的に推奨されます。
後者は命令形であるため、エラーが発生しやすいためです。
しかし、ref.listenは大きなリファクタリングを行わず、迅速にロジックの追加することに役立つことがあります。

ref.watch の例を ref.listen を使用して書き換えることができます。

Dart
@riverpod
int example(ExampleRef ref) {
  ref.listen(otherProvider, (previous, next) {
    print('Changed from: $previous, next: $next');
  });

  return 0;
}

備考)

provider のビルド段階で’ref.listenを使用することは安全です。
provider が再計算されると、以前のリスナーは削除されます。

代わりに、ref.listenの戻り値を使用して、望むときにリスナーを手動で削除することもできます。

ref.read メソッド

最後のオプションは ref.read です。
provider の最新の state を返す点でref.watchと似ています。 しかし、ref.watchとは異なり、provider をリッスンしません。

そのため、ref.readは Notifiers のメソッド内、ref.watchを使用できない場所でのみ使用する必要があります。

Dart
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  int build() {
    // 悪い例です。 ここで "read "を使ってはいけません。
    ref.read(otherProvider);

    return 0;
  }

  void increment() {
    // ここで"read"を使用することは問題ありません。
    ref.read(otherProvider); 
  }
}

注意)

provider を監視しないため、ref.read を provider で使用する際には注意が必要です。
リスナーがいない場合、provider は state を破棄する可能性があります。

キャッシュのクリアとstate破棄への反応

これまでに、state を作成、または更新する方法を見てきました。
しかし、state が破棄されるタイミングについてはまだ話していません。

Riverpod は state 破棄(disposal)と相互作用する様々な方法を提供します。
これは、状態の破棄を遅らせることから破棄に反応することまで多岐にわたります。

状態はいつ破棄されるのか、そしてどう変更するのか?

コード生成を使うことで、デフォルトでは provider がリッスンを停止すると state が破棄されます。
これは、リスナーがフレーム全体に対してアクティブなリスナーがないときに発生します。

この動作はkeepAlive: trueを使うことで回避できます。
全てのリスナーが削除されたときに state が破棄されることを防ぎます。

Dart
// stateが自動破棄されることを無効にするため、アノテーションに"keepAlive: true"を指定します。
@Riverpod(keepAlive: true)
int example(ExampleRef ref) {
  return 0;
}

注記)

自動破棄を有効/無効にしても、provider が再計算されたときに状態が破棄されるかどうかには影響しません。
state は常に provider が再計算されるときに破棄されます。

注意)

provider がパラメータを受け取る場合、自動破棄を有効にすることを推奨します。
そうしないと、パラメータの組み合わせごとに state が作成され、メモリリークを引き起こす可能性があります。

state 破棄への反応

Riverpod には、state が破棄される方法がいくつかあります:

  • provider が使用されなくなり、”auto dispose”モードになっている場合(詳細は後述)。
    この場合、provider と関連するすべての state が破棄されます。
  • provider が再計算される場合(ref.watchなど)。
    この場合、以前の state が破棄され、新しい state が作成されます。

どちらの場合も、state が破棄されたときいくつかのロジックを実行したいかもしれません。
これはref.onDisposeを使用することで実現できます。
このメソッドを使用すると、state が破棄されるたびにリスナーを登録できます。

例えば、アクティブなStreamControllerを閉じるためにこのメソッドを使用できます:

Dart
@riverpod
Stream<int> example(ExampleRef ref) {
  final controller = StreamController<int>();

  // stateが破棄されると、streamControllerを閉じます。
  ref.onDispose(controller.close);

  // TO-DO: StreamControllerに値をプッシュする。
  return controller.stream;
}

注意)

ref.onDisposeのコールバックは副作用を引き起こしてはなりません。
onDispose 内で provider を変更すると予期しない動作を引き起こす可能性があります。

備考)

他にも便利なライフサイクルイベントがあります:

  • ref.onCancelは、provider の最後のリスナーが削除されたときに呼び出されます。
  • ref.onResumeは、onCancelが呼び出された後に新しいリスナーが追加されたときに呼び出されます。

備考)

ref.onDisposeは何回でも呼び出すことができます。
provider 内の各破棄可能なオブジェクトごとに 1 回ずつ呼び出すことができます。
この方法だと、何かを破棄し忘れた場合に簡単に見つけることができます。

ref.invalidateを使うことで provider の破棄を強制する

時々、provider を強制的に破棄したい場合があります。
他の provider やウィジェットからref.invalidateを使用して実行できます。

ref.invalidateを使用すると、現在の provider の state が破壊されます。
次の二つの結果が考えられます:

  • provider がリッスンされている場合、新しい state が作成されます。
  • provider がリッスンされていない場合、provider が完全に破棄されます。
Dart
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    return ElevatedButton(
      onPressed: () {
        // タップすると、providerを破棄します。
        ref.invalidate(someProvider);
      },
      child: const Text('dispose a provider'),
    );
  }
}

備考)

ref.invalidateSelfを使うことで provider が自身を破棄することができます。
ただし、この場合、常に新しい state が作成されます。

ヒント)

パラメータを受け取る provider を無効にしようとする場合、
特有のパラメータの組み合わせの provider を無効にするか、
全てのパラメータの組み合わせを一度に無効にすることができます:

Dart
@riverpod
String label(LabelRef ref, String userName) {
  return 'Hello $userName';
}

// ...

void onTap() {
  // このproviderの全ての組み合わせ可能なパラメータを無効にします。
  ref.invalidate(labelProvider);
  // 指定した組み合わせのみを無効にします。
  ref.invalidate(labelProvider('John'));
}

ref.keepAliveを使用した細かい破棄制御

前述の通り、自動破棄が有効な場合、provider がフルフレームの間リスナーを持たないと state が破棄されます。

しかし、この動作をより細かく制御したい場合があります。
例えば、成功したネットワークリクエストの state を保持し、
失敗したリクエストをキャッシュしないようにしたい場合です。

これは、自動破棄を有効にした後でref.keepAliveを使用することで実現できます。
これを使用すると、状態が自動的に破棄されるタイミングを決定できます。

Dart
@riverpod
Future<String> example(ExampleRef ref) async {
  final response = await http.get(Uri.parse('https://example.com'));
  // リクエストが成功した後のみ、providerを生かしておく。
  // リクエストが失敗した(例外を投げた)場合、providerがリッスンされなくなると、stateは破棄される。
  ref.keepAlive();

  // `link`を使うことで、自動破棄の動作を元に戻すことができる:
  // link.close();

  return response.body;
}

注記)

provider が再計算されると、自動破棄が再度有効になります。

ref.keepAliveの戻り値を使用して、自動破棄に戻すことも可能です。

例: 特定の時間だけ state を保持する

現在、Riverpod は特定の時間だけ state を保持するための仕組みを提供していません。
しかし、これまでに見てきたツールを使用して、そのような機能を簡単に実装して再利用可能にすることができます。

Timer + ref.keepAliveを使用して、特定の時間だけ状態を保持できます。
このロジックを再利用可能にするために、拡張メソッド(extension method)で実装することができます:

Dart
extension CacheForExtension on AutoDisposeRef<Object?> {
/// [duration] の間 providerを維持する。
  void cacheFor(Duration duration) {
    // stateが破壊されるのを防ぐ。
    final link = keepAlive();
    // 期間経過後、自動破棄を再度有効にする。
    final timer = Timer(duration, link.close);

    // オプション:providerが再計算される場合(ref.watch など)、
    // 保留中のタイマーをキャンセルする.
    onDispose(timer.cancel);
  }
}

次に、以下のように使用できます:

Dart
@riverpod
Future<Object> example(ExampleRef ref) async {
  /// 5分間、stateを維持する
  ref.cacheFor(const Duration(minutes: 5));

  return http.get(Uri.https('example.com'));
}

このロジックは、ニーズに合わせて調整することができます。
例えば、ref.onCancel/ref.onResumeを使用して、provider が特定の時間聞かれていなかった場合にのみ状態を破棄するようにすることができます。

providerの早期初期化

全ての provider はデフォルトでは遅延初期化されます。
これは、provider が初めて使用されるときに初期化されることを意味します。
これは、アプリケーションの特定の部分でのみ使用される provider に便利です。

残念ながら、Dart の動作(tree shaking のため)により、provider を早期に初期化する必要があるとフラグを立てる方法はありません。
ただし、アプリケーションの root で早期初期化したい provider を強制的に読み取る方法があります。

おすすめのアプローチは、ProviderScopeの直下に配置された Consumer で provider を単にwatchすることです:

Dart
void main() {
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return const _EagerInitialization(
      // TODO: アプリをレンダリングする。
      child: MaterialApp(),
    );
  }
}

class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // providerをwatchすることで早期初期化する。
    // "watch"を使うことでproviderが維持され、破棄されません。
    ref.watch(myProvider);
    return child;
  }
}

注記)

初期化用の Consumer を”MyApp”やパブリックウィジェットに配置することを検討してください。
これにより、main からロジックを削除し、テストでも同じ動作を使用できるようになります。

FAQ

provider が変更されたときにアプリケーション全体が再構築されるのではないですか?

いいえ、そうではありません。
上記のサンプルでは、早期初期化を行う Consumer が別のウィジェットとして、childを返す以外に何もしません。

MaterialApp自体をインスタンス化するのではなく、childを返すことが重要です。
つまり、_EagerInitializationが再構築されても、child変数は変更されません。
そして、ウィジェットが変更されない場合、Flutter はウィジェットを再構築しません。

したがって、別のウィジェットがその provider をリッスンしていない限り、_EagerInitialization のみが再構築されます。

このアプローチを使用して、ローディング状態やエラー状態をどのように処理できますか?

Consumerで通常処理するのと同じように、ローディング/エラー状態を処理できます。
_EagerInitializationは provider が “loading” 状態にあるかどうかを確認し、そうであれば childの代わりに CircularProgressIndicatorを返すことができます:

Dart
class _EagerInitialization extends ConsumerWidget {
  const _EagerInitialization({required this.child});
  final Widget child;

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final result = ref.watch(myProvider);

    // エラー状態とローディング状態を処理します。
    if (result.isLoading) {
      return const CircularProgressIndicator();
    } else if (result.hasError) {
      return const Text('Oopsy!');
    }

    return child;
  }
}

ローディング/エラー状態を処理しましたが、他の Consumers も AsyncValue を受け取ります!すべてのウィジェットでローディング/エラー状態を処理しなくても済む方法はありますか?

provider がAsyncValueを公開しないようにする代わりに、ウィジェットがAsyncValue.requireValueを使用することができます。
これにより、パターンマッチングを行わずにデータを読み取ることができます。
そして、バグが発生した場合は明確なメッセージとともに例外がスローされます。

Dart
// 早期初期化される provider
@riverpod
Future<String> example(ExampleRef ref) async => 'Hello world';

class MyConsumer extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final result = ref.watch(exampleProvider);

    /// providerが正しく早期初期化されれば "requireValue"でデータを直接読み込むことができます。
    return Text(result.requireValue);
  }
}

注記)

これらの場合(スコープに依存する)にローディング/エラー状態を公開しない方法もありますが、一般的にはそれは推奨されません。
2 つのプロバイダーを作成し、オーバーライドを使用するという複雑さは、手間に見合いません。

providerのテスト

Riverpod API の重要な部分は、provider を単独でテストする能力です。

適切なテストスイートを作成するためには、いくつかの課題を克服する必要があります:

  • テストは state を共有すべきではありません。
    これは、新しいテストが前のテストの影響を受けないことを意味します.
  • テストは特定の機能をモックできる能力を提供し、望んだ state を実現します。
  • テスト環境は可能な限り実際の環境に近いものであるべきです。

幸いなことに、Riverpod はこれらの目標を達成することを容易にします。

テストの設定

Riverpod でテストを定義するとき、大きく 2 つのシナリオがあります:

  • ユニットテスト、通常は Flutter 依存関係がないテスト。
    これは provider の動作を単独でテストするときに便利です。
  • ウィジェットテスト、通常は Flutter 依存関係があるテスト。
    provider を使用するウィジェットの動作をテストするのに便利です。

ユニットテスト

ユニットテストは package:test のtest 関数を使用して定義されます。

他のテストとの主な違いは、ProviderContainerオブジェクトを作成する必要があるという点です。
このオブジェクトは provider との対話することを可能にします。

ProviderContainerオブジェクトの作成と破棄のためのテストユーティリティを作成することが推奨されます:

Dart
import 'package:riverpod/riverpod.dart';
import 'package:test/test.dart';

/// [ProviderContainer]を作成し、テスト終了時に自動破棄するテストユーティリティです。
ProviderContainer createContainer({
  ProviderContainer? parent,
  List<Override> overrides = const [],
  List<ProviderObserver>? observers,
}) {
  // ProviderContainerを作成し、オプションでパラメータを指定します。
  final container = ProviderContainer(
    parent: parent,
    overrides: overrides,
    observers: observers,
  );

  // テスト終了時、containerを破棄します。
  addTearDown(container.dispose);

  return container;
}

次に、このユーティリティを使用して testを定義できます:

Dart
void main() {
  test('Some description', () {
    // このテストのためにProviderContainerを作成します。
    // テスト間でのProviderContainerの共有はしてはいけません。
    final container = createContainer();

    // TODO: アプリのテストを行うためにcontainerを使用します。
    expect(
      container.read(provider),
      equals('some value'),
    );
  });
}

ProviderContainer を持つことで、次の方法で provider を読み取ることができます:

  • container.readを使用して provider の現在の値を読み取る。
  • container.listenを使用して provider をリッスンし、変更を通知する。

注意)

provider が自動破棄される場合は、container.readの使用に注意してください。 provider がリッスンされていない場合、テストの途中でその状態が破棄される可能性があります。

その場合は、container.listenの使用を検討してください。 この戻り値を使用すると、provider の現在の値を読み取ることができますが、テストの途中で provider が破棄されないことも保証されます:

Dart
    final subscription = container.listen<String>(provider, (_, __) {});

    expect(
      // `container.read(provider)`と同等です。
      // しかし、"subscription"が破棄されない限り、providerは破棄されません。
      subscription.read(),
      'Some value',
    );
    

ウィジェットテスト

ウィジェットテストはpackage:flutter_testのtestWidgets関数を使用して定義されます。

この場合、通常のウィジェットテストとの最大の違いは、tester.pumpWidgetのルートにProviderScopeウィジェットを追加する必要があるという点です:

Dart
void main() {
  testWidgets('Some description', (tester) async {
    await tester.pumpWidget(
      const ProviderScope(child: YourWidgetYouWantToTest()),
    );
  });
}

これは Flutter アプリで Riverpod を有効にする時と似ています。

次に、testerを使用してウィジェットと対話できます。
または provider と対話したい場合は、ProviderContainerを取得できます。
これは ProviderScope.containerOf(buildContext)を使用して取得できます。
したがって、testerを使用すると次のように書くことができます:

Dart
    final element = tester.element(find.byType(YourWidgetYouWantToTest));
    final container = ProviderScope.containerOf(element);

次に、これを使用して provider を読み取ることができます。以下は完全な例です:

Dart
void main() {
  testWidgets('Some description', (tester) async {
    await tester.pumpWidget(
      const ProviderScope(child: YourWidgetYouWantToTest()),
    );

    final element = tester.element(find.byType(YourWidgetYouWantToTest));
    final container = ProviderScope.containerOf(element);

    // TODO: providersと対話する
    expect(
      container.read(provider),
      'some value',
    );
  });
}

provider のモック

これまでに、テストの設定方法と provider と基本的なやりとりについて見てきました。
しかし、場合によっては provider をモック(mock)したいことがあります。

全ての provider は追加の設定なしでモックすることができます。
これは、、ProviderScopeまたはProviderContainerのoverridesパラメータを指定することで可能です。

次の provider を考えてみましょう:

Dart
// providerの初期化
@riverpod
Future<String> example(ExampleRef ref) async => 'Hello world';

これを次のようにモックできます:

Dart
    // ユニットテストでは以前の "createContainer" ユーティリティを再利用します。
    final container = createContainer(
      //  モックするproviderのリストを指定することができる:
      overrides: [
        // この場合 "exampleProvider"をモック(mock)化しています
        exampleProvider.overrideWith((ref) {
          // この関数はproviderの典型的な初期化関数です。
          // ここで通常は "ref.watch"を呼び出し、初期状態を返します。

          // デフォルトの "Hello world "をカスタム値に置き換えてみましょう。
          // 次に `exampleProvider`とやりとりするとこの値が返されます。
          return 'Hello from tests';
        }),
      ],
    );

    // ProviderScopeを使ったウィジェットテストでも同じことができます:
    await tester.pumpWidget(
      ProviderScope(
        // ProviderScopesには、まったく同じ "overrides "パラメーターがあります。
        overrides: [
          // 前述と同じです。
          exampleProvider.overrideWith((ref) => 'Hello from tests'),
        ],
        child: const YourWidgetYouWantToTest(),
      ),
    );

provider の変更を監視する

テストでProviderContainerを取得して、それを利用して provider を”listen”することができます:

Dart
    container.listen<String>(
      provider,
      (previous, next) {
        print('The provider changed from $previous to $next');
      },
    );

次に、これをmockitoやmocktailなどのパッケージと組み合わせて、verify API を使用できます。
または、よりシンプルに全ての変更をリストに追加し、それをアサート(assert)することもできます。

非同期 provider の待機

Riverpod では、provider が Future/Stream を返す場合が非常に多いです。
その場合、テストでは非同期操作が完了するのを待つ必要があります。

その方法の一つは、プロバイダの.futureを読み取ることです:

Dart
    // TODO: アプリのテストを行うためにcontainerを使用します。
    // 期待値は非同期なので、"expectLater" を使うべきである。
    await expectLater(
      // "provider" の代わりに "provider.future"で読み取ります。
      // これは非同期providerの場合に可能で、providerの値を決めるfutureを返します。
      container.read(provider.future),
      // futureが期待値で決まることを確認できます。
      // あるいはエラーの場合 "throwsA"を使用できます。
      completion('some value'),
    );

Notifiers のモック

一般的には Notifiers をモックすることは推奨されません。
なぜなら Notifiers は自己インスタンス化できず、provider の一部としてのみ機能するためです。

代わりに、Notifiers のロジックに抽象化のレベルを導入し、その抽象化をモックすることを検討すべきです。
例えば、Notifier をモック化するよりも、Notifier がデータを取得するために使用する “repository”をモックすることができます。

それでも Notifier をモックしたい場合、特別な考慮が必要です。:
モックは元の Notifier ベースクラスをサブクラス化する必要があります:
インターフェースが壊れる可能性があるため、Notifier を “implement” することはできません。

そのため、Notifier をモックするときは、次のような mockito コードを書かないでください:

Dart
class MyNotifierMock with Mock implements MyNotifier {}

代わりに次のように書いて下さい:

Dart
@riverpod
class MyNotifier extends _$MyNotifier {
  @override
  int build() => throw UnimplementedError();
}

// モックはNotifierのベースクラスをサブクラス化する必要があります。
class MyNotifierMock extends _$MyNotifier with Mock implements MyNotifier {}

これを機能させるためには、モックをモックする Notifier と同じファイルに配置する必要があります。
そうしないと、_$MyNotifierクラスにアクセスできません。

次に、Notifier を使用するには次のようにします:

Dart
void main() {
  test('Some description', () {
    final container = createContainer(
      // providerをオーバーライドして、モックNotifierを作成します。
      overrides: [myNotifierProvider.overrideWith(MyNotifierMock.new)],
    );

    // 次にContainerを通してモックNotifierを取得します:
    final notifier = container.read(myNotifierProvider.notifier);

    // 本物のNotifierと同じようにやりとりできます:
    notifier.state = 42;
  });
}

ログ記録とエラーレポート

Riverpod は provider ツリー内で発生するすべてのイベントを自然に監視する方法を提供します。
これは全てのイベントを記録したり、リモートサービスにエラーを報告するために使用できます。

これはProviderObserverクラスを使用し、ProviderScope/ProviderContainerを渡すことで実現されます。

ProviderObserver の定義

ProviderObserverは拡張すべきクラスです。
イベントをリッスン(listen)するために、オーバーライド(override)できるさまざまなメソッドを提供します:

  • didAddProvider, provider が tree に追加された時に呼び出されます
  • didUpdateProvider, provider が更新された時に呼び出されます
  • didDisposeProvider, provider が破棄された時に呼び出されます
  • providerDidFail, synchronous provider がエラーをスローした時に呼び出されます

Dart
class MyObserver extends ProviderObserver {
  @override
  void didAddProvider(
    ProviderBase<Object?> provider,
    Object? value,
    ProviderContainer container,
  ) {
    print('Provider $provider was initialized with $value');
  }

  @override
  void didDisposeProvider(
    ProviderBase<Object?> provider,
    ProviderContainer container,
  ) {
    print('Provider $provider was disposed');
  }

  @override
  void didUpdateProvider(
    ProviderBase<Object?> provider,
    Object? previousValue,
    Object? newValue,
    ProviderContainer container,
  ) {
    print('Provider $provider updated from $previousValue to $newValue');
  }

  @override
  void providerDidFail(
    ProviderBase<Object?> provider,
    Object error,
    StackTrace stackTrace,
    ProviderContainer container,
  ) {
    print('Provider $provider threw $error at $stackTrace');
  }
}

ProviderObserver の使用

observer を定義したので、これを使用する必要があります。
これを行うには、ProviderScope または ProviderContainerに渡す必要があります:

Dart
runApp(
  ProviderScope(
    observers: [
      MyObserver(),
    ],
    child: MyApp(),
  )
);
Dart
final container = ProviderContainer(
  observers: [
    MyObserver(),
  ],
);

FAQ

コミュニティからよく寄せられる質問をいくつか紹介します。

ref.refreshと ref.invalidateの違いは何ですか?

ref には provider を再計算するための 2 つのメソッドがあることに気付いたかもしれませんが、それらの違いについて疑問に思うかもしれません。

実はそれほど複雑ではありません。ref.refresh は invalidate と read を組み合わせたシンタックスシュガーに過ぎません。

Dart
T refresh<T>(provider) {
  invalidate(provider);
  return read(provider);
}

再計算された後の provider の新しい値が気にならない場合は、invalidateを使用します。 新しい値が必要な場合は、代わりにrefreshを使用します。

備考)

このロジックはリントルールを通して自動的に適用されます。
返された値を使わずにref.refreshを使うと警告が表示されます。

動作の主な違いは、invalidate した直後に provider を読み込むことで、provider が即座に再計算されることです。
一方、invalidateしても直後に読み込まなければ、更新は後でトリガーされます。

“後で”の更新は一般的には次のフレームの開始時に行われます。 しかし、現在リッスンされていない provider が invalidate された場合、再度リッスンされるまで更新されません。

Ref と WidgetRef の間に共通のインターフェースがないのはなぜですか?

Riverpod は意図的に Ref と WidgetRefを分離しています。
これはどちらかに依存するコードを書くことを避けるためです。

一つの問題は、RefとWidgetRefが似ているように見えるが微妙な違いがあるということです。
両方に依存するコードは、見つけることが難しい点で信頼性に欠けます。

同時に、WidgetRefに依存することはBuildContextに依存することと同じです。
これは事実上、UI レイヤーにロジックを配置することと同じであり、推奨されません。


このようなコードは常にRefを使用するようにリファクタリングすべきです。

この問題の解決策は、ロジックをNotifierに移動することです(詳細は副作用の実行を参照)。
そして、そのロジックをその Notifier のメソッドにします。

これにより、ウィジェットがこのロジックを呼び出したい場合、以下のように書けます:

Dart
ref.read(yourNotifierProvider.notifier).yourMethod();

yourMethodは他の provider とのやりとりを行うために、NotifierのRefを使用します。

なぜ StatelessWidget を使う代わりに ConsumerWidget を拡張する必要があるのですか?

これは、InheritedWidgetの API の制限によるものです。

いくつかの問題があります:

  • InheritedWidgetの”on change”リスナーを実装することはできません。
    つまり、ref.listenなどをBuildContextと一緒に使用することはできません。

    State.didChangeDependenciesが最も近いものですが、信頼性がありません。
    問題は、ウィジェットツリーが GlobalKey を使用する場合((内部的に一部の Flutter ウィジェットが既にそうしています)、依存関係が変更されなくてもライフサイクルがトリガーされる可能性があります。
  • InheritedWidgetをリッスンしているウィジェットはリッスンを停止しません。
    これは、”テーマ”や”メディアクエリ”などの純粋なメタデータには通常問題ありません。
    ビジネスロジックの場合、これは問題です。
    paginated API を表現するために provider を使用するとします。
    ページオフセットが変更されたときに、以前に表示されていたページにウィジェットがリッスンし続けるのを望まないでしょう。
  • InheritedWidgetには、ウィジェットがいつリッスンを停止するかを追跡する方法がありません。
    Riverpod は provider がリッスンされているかどうかを追跡するために時々依存することがあります。

この機能は、自動破棄メカニズムやプロバイダに引数を渡す機能の両方にとって重要です。
これらの機能が Riverpod を非常に強力にしています。

将来的には、これらの問題が解決されるかもしれません。
その場合、Riverpod は Refの代わりに BuildContextを使用するように移行するでしょう。
これにより、ConsumerWidget の代わりに StatelessWidget を使用できるようになります。
しかし、それは次の機会になります!

なぜ hooks_riverpod は flutter_hooks をエクスポートしないのですか?

これは、適切なバージョン管理の慣行を尊重するためです。

flutter_hooksなしでhooks_riverpodを使用することはできませんが、 両方のパッケージは独立してバージョン管理されています。
一方で重大な変更が発生することがありますが、他方では発生しないことがあります。

Riverpod は一部のケースでフィルタの更新に==ではなくidenticalを使用するのはなぜですか?

Notifiers はフィルタの更新に==ではなくidenticalを使用します。

これは、Riverpod ユーザーが Freezed/built_value などのコードジェネレータを使用して copyWith の実装を行うことが多いためです。
これらのパッケージはオブジェクトを比較するために==をオーバーライドします。
オブジェクトの比較は非常にコストがかかります。
“Business logic”モデルには多くのプロパティが含まれる傾向があります。
さらに悪いことに、リスト、マップなどのコレクションもあります。

同時に、複雑な”ビジネス”オブジェクトを利用する時、ほとんどのstate = newState呼び出しは常に通知を発生させます。
(そうでなければ setter を呼び出す必要はありません)
一般的に、現在の状態と新しい状態が等しい場合に state = newState を呼び出すのは、プリミティブオブジェクト(int、enum、string など)に対してです。
これらのオブジェクトは”デフォルトで正規化されています”。このようなオブジェクトが等しい場合、通常は”同一(identical)”であることも意味します。

したがって、Riverpod がフィルタの更新にidenticalを使用するのは、両方の世界にとって良いデフォルトを持つ試みです。
オブジェクトのフィルタリングにこだわらず、コードジェネレータがデフォルトで==オーバーライドを生成するため、==がコストがかかる場合、identicalを使用することで効率的なリスナ-通知方法を提供します。 同時に、単純なオブジェクトの場合、identicalは冗長な通知を正しくフィルタリングします。

最後に、Notifiers のupdateShouldNotifyメソッドをオーバーライドして動作を変更することもできます。

全ての provider を一度にリセットする方法はありますか?

いいえ、全ての provider を一度にリセットする方法はありません。

これは意図的に行われており、アンチパターンと見なされます。
すべての provider を一度にリセットすると、リセットする意図のない provider もリセットされることがよくあります。

これは一般的にユーザがログアウトする時にアプリケーションの状態をリセットしたい場合に尋ねられます。
この機能を望む場合、代わりにユーザの状態に依存するすべてのものをref.watchで”user” provider に設定する必要があります。

その場合、ユーザーがログアウトすると、これに依存するすべての provider が自動的にリセットされますが、その他のものはそのままになります。

“Cannot use “ref” after the widget was disposed”というエラーが発生しました。何が問題ですか?

おそらく”Bad state: No ProviderScope found”も見たことがあるかもしれませんが、これは同じ問題の古いエラーメッセージです。

このエラーは、ウィジェットがマウントされていない状態で ref を使用しようとしたときに発生します。
これは一般的にawaitの後に発生します:

Dart
ElevatedButton(
  onPressed: () async {
    await future;
    ref.read(...); // ここで「Cannot use "ref" after the widget was disposed」というエラーが発生する可能性があります
  }
)

解決策は、BuildContextと同様に、refを使用する前にmountedを確認することです:

Dart
ElevatedButton(
  onPressed: () async {
    await future;
    if (!context.mounted) return;
    ref.read(...); // これでエラーは発生しません
  }
)

DO/DON’T

コードの保守性を高めるために、以下の Riverpod のベストプラクティスを参照してください。

このリストは網羅的ではなく、変更される可能性があります。
提案がある場合はいつでもissue を開いてください。

このリストの項目には特定の順序はありません。

これらの推奨事項の多くは、riverpod_lintを使用して強制することができます。
インストール手順については、riverpod_lint/custom_lint の有効化を参照してください。

ウィジェットで provider の初期化をしない

providers は自身で初期化するべきです。 ウィジェットのような外部要因で初期化されるべきではありません。
これを怠ると、競合状態や予期しない動作が発生する可能性があります。

DON’T

Dart
class WidgetState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    // Bad: providerは自分で初期化するべきです
    ref.read(provider).init();
  }
}

考慮すべきこと

この問題に対する”万能”な解決策はありません。
初期化ロジックが provider 外の要素に依存する場合、ロジックを置くのに適切な場所はボタンの onPressed メソッドです。

Dart
ElevatedButton(
  onPressed: () {
    ref.read(provider).init();
    Navigator.of(context).push(...);
  },
  child: Text('Navigate'),
)

ローカルウィジェットの状態に provider を使用しない

provider は共通のビジネス状態(state)のために設計されています。
ローカルウィジェットの状態(state)、例えば次のようなものには適していません:

  • フォームの状態を保持する
  • 現在選択されているアイテム
  • アニメーション
  • 一般に Flutter が”コントローラ”で扱うすべてのもの(例:TextEditingController)

ローカルウィジェットの状態を扱う方法を探している場合、代わりに flutter_hooks を使用することをお勧めします。

これが推奨されない理由の一つは state がルート(route)にスコープされることがよくあるためです。

これを怠ると、新しいページが前のページの状態を上書きするため、アプリの戻るボタンが壊れる可能性があります。

provider の初期化の間に副作用を行わない

provider は一般的に”read”操作を表すために使用されるべきです。
フォームの送信などの”write”操作には使用されるべきではありません。

provider をこのような操作に使用すると、副作用がスキップされたり、予期しない動作が発生する可能性があります。

副作用のローディング/エラー状態を処理する方法を知りたい場合は、副作用を参照してください。

DON’T:

Dart
final submitProvider = FutureProvider((ref) async {
  final formState = ref.watch(formState);

  // Bad: providerは"write"操作に使われるべきでありません。
  return http.post('https://my-api.com', body: formState.toJson());
});

静的に既知の provider で ref.watch/read/listen(および類似の API)を使用することを推奨

Riverpod はリントルールを有効にすることを強く推奨します(riverpod_lint参照)。
しかし、lint が効果的であるためには、コードが静的に解析可能な方法で書かれている必要があります。

これを怠ると、バグを見つけるのが難しくなったり、lint で誤検出が発生する可能性があります。

Do:

Dart
final provider = Provider((ref) => 42);

...

// OK providerが静的に既知であるためOK
ref.watch(provider);

Don’t:

Dart
class Example extends ConsumerWidget {
  Example({required this.provider});
  final Provider<int> provider;

  @override
  Widget build(context, ref) {
    // 静的解析が`provider`が何であるかを知ることができないため悪い例
    ref.watch(provider);
  }
}

動的に provider を作成しない

provider はトップレベルの final 変数であるべきです。

Do:

Dart
final provider = Provider<String>((ref) => 'Hello world');

Don’t:

Dart
class Example {
  // サポートされていない操作。メモリリークや予期しない動作の原因となる可能性があります。
  final provider = Provider<String>((ref) => 'Hello world');
}

備考)

provider を静的な final 変数として作成することは許可されていますが、コードジェネレータではサポートされていません。

(次の記事はこちら)

コメントを残す