メインコンテンツに進む

最初のprovider/ネットワークリクエストを作成する

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

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

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

ProviderScope の設定

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

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

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

注記

完全なインストール手順(riverpod_lintのインストールやコードジェネレータの実行など)については、 開始方法をご覧ください。

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

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

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

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

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

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

モデルの定義

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

一般的に、JSON デコードを処理する際には、Freezedjson_serializable などのコード生成パッケージを使用することをおすすめします。
もちろん、手動で処理することも可能です。

どちらにせよ、最終的には次のようなモデルになります:

activity.dart

import 'package:freezed_annotation/freezed_annotation.dart';

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

/// `GET /api/activity` エンドポイントのレスポンス
///
/// `freezed`と `json_serializable`を使って定義されています。

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 を定義する構文は次のようになります:

@riverpod
Result myFunction(Ref ref) {
  <your logic here>
}
annotation

すべての provider には @riverpod または @Riverpod()の annotation が必要です。
この annotation はグローバル関数またはクラスに付けることができます。
この annotation を通じて provider を構成することができます。

たとえば, @Riverpod(keepAlive: true)と書くことで "auto-dispose"を無効にすることができます(後で説明します)。

関数annotation

関数 annotation の名前は、provider との対話方法を決定します。
例えばmyFunctionという関数の場合、 myFunctionProvider 変数が生成されます。

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

この関数は provider が最初に読み取られたときに呼び出されます。
その後の読み取りは関数を再度呼び出すのではなく、キャッシュされた値を返します。

Ref

他の provider と対話するために使用されるオブジェクトです。
すべての provider が持っています。provider 関数のパラメーターとして、または Notifier のプロパティとして存在します。
このオブジェクトのタイプは function/class の名前によって決定されます。

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

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

provider.dart

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

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

/// これにより、`activityProvider`という名前のproviderが作成され、
/// この関数の結果をキャッシュします。

Future<Activity> activity(Ref 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というカスタムウィジェットを使用することです。 ConsumerBuilderに似たウィジェットですが、"ref"を提供するという追加の利点があります。 これにより、UI が provider を読み取ることができます。 次の例はConsumerの使用方法を示しています:

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});


Widget build(BuildContext context) {
return Consumer(
builder: (context, ref, child) {
// activityProviderを読み取ります。これにより、ネットワークリクエストが
// まだ開始されていない場合は開始されます。
// ref.watchを使用することで、このウィジェットは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/StatefulWidgetConsumerを返す代わりに、ConsumerWidget/ConsumerStatefulWidgetを定義できます。 ConsumerWidgetConsumerStatefulWidgetは、StatelessWidget/StatefulWidgetConsumerの融合です。 これらは元のカウンターパートと同じように動作しますが、"ref"を提供するという利点があります。

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


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


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

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

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


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


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

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

void initState() {
super.initState();

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


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

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

Flutter_hooks の考慮事項: HookWidgetConsumerWidgetの組み合わせ

注意

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

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

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

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


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


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

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

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