メインコンテンツに進む

プロバイダのステートを組み合わせる

本セクションの前に「プロバイダとは」のセクションに目を通していただくことをおすすめします。 ここでは複数のプロバイダのステート(状態)を組み合わせる方法をご紹介します。

プロバイダのステートを組み合わせる

これまでプロバイダの簡単な使用方法をご紹介しましたが、実際の開発ではプロバイダを他のプロバイダと組み合わせる場面が多いかと思います。

このような場面では、プロバイダのコールバック関数に渡される ref オブジェクトの watch メソッドを使用してください。

例として、次のようなプロバイダがあるとします。

final cityProvider = Provider((ref) => 'London');

この cityProvider を利用する別のプロバイダを作成します。

final weatherProvider = FutureProvider((ref) async {
// `ref.watch` により他のプロバイダの値を取得・監視します。
// 利用するプロバイダ(ここでは cityProvider)を引数として渡します。
final city = ref.watch(cityProvider);

// 最後に `cityProvider` の値をもとに行った計算結果を返します。
return fetchWeather(city: city);
});

以上です。これで他のプロバイダの値に依存するプロバイダを作ることができました。

よくある質問

依存先のプロバイダの値が変わったら、その依存元のプロバイダはどうなりますか?

依存先のプロバイダが StateNotifierProvider であったり、ProviderContainer.refreshref.refresh により更新された場合、その値は変わることがあります。

しかしこのような場合でも値の取得に watch を使っていれば、Riverpod は値の変化を検出して 自動的に 依存元のプロバイダの値を再評価してくれます。

例えば Todo リストを値として外部に公開する、次のような StateNotifierProvider があるとします。

class TodoList extends StateNotifier<List<Todo>> {
TodoList(): super(const []);
}

final todoListProvider = StateNotifierProvider((ref) => TodoList());

そしてこの Todo リストを完了タスク(あるいは未完了タスク)のみのリストに変換して、UI 側に表示させたいとします。

このような機能を実現する方法としては、一般的に次のようなものが考えられます。

  • 現在選択されているフィルタの種類(enum)を公開する StateProvider を作成する。

    enum Filter {
    none,
    completed,
    uncompleted,
    }

    final filterProvider = StateProvider((ref) => Filter.none);
  • フィルタの種類と Todo リストを組み合わせることで新たな Todo リストを生成し、値として外部に公開する第3のプロバイダを作成する。

    final filteredTodoListProvider = Provider<List<Todo>>((ref) {
    final filter = ref.watch(filterProvider);
    final todos = ref.watch(todoListProvider);

    switch (filter) {
    case Filter.none:
    return todos;
    case Filter.completed:
    return todos.where((todo) => todo.completed).toList();
    case Filter.uncompleted:
    return todos.where((todo) => !todo.completed).toList();
    }
    });

これで UI 側にこの filteredTodoListProvider を監視させることで、その値の変化に応じて Todo リストを表示することができるようになりました。 フィルタの種類もしくは Todo リストの内容が変われば、UI も自動的に再構築されます。

この手法を使って作成された Todo アプリのサンプルコードはこちらでご覧いただけます。

備考

依存するプロバイダの値が変わると自動で値が再評価されるプロバイダは Provider だけではありません。 すべてのプロバイダがこの性質を持ちます。

例えば watchFutureProvider を組み合わせて、検索クエリや検索設定をステートの算出に組み込んだ検索機能を実現することもできます。

// 検索クエリ
final searchProvider = StateProvider((ref) => '');

/// ホスト名等の検索設定
final configProvider = StreamProvider<Configuration>(...);

final charactersProvider = FutureProvider<List<Character>>((ref) async {
final search = ref.watch(searchProvider);
final configs = await ref.watch(configProvider.future);
final response = await dio.get('${configs.host}/characters?search=$search');

return response.data.map((json) => Character.fromJson(json)).toList();
});

これで検索設定や検索クエリに変更があるたびに、外部サービスからキャラクターのリストを自動で取得してくれるようになりました。

プロバイダ内で別のプロバイダの値を監視せずに、取得だけする方法はないですか?

プロバイダ内で別のプロバイダの値を取得して利用したいけど、プロバイダの更新は避けたいというケースに遭遇したことはないですか。

このようなケースに遭遇するプロバイダの典型例としては、Repository オブジェクトを値として公開するプロバイダでしょう。 Repository は認証用トークンを利用して HTTP リクエストを実行するため、まず認証用トークンを取得する必要があります。 一つの方法として、プロバイダ内で watch を使って別のプロバイダからトークンを取得し、トークンが変わるたびに新しく Repository を作り直すことも可能です。 しかし、これは無駄が多いように思えます。

それなら read を使うのはどうでしょうか? この場合はトークンが変わったとしても Repository はそのままなので、リクエストの実行ができません。

このような場合は Repository オブジェクトに ref.read を渡してください。 そうすることで Repository オブジェクトは自身のタイミングで、別のプロバイダからトークンを取得できるようになります。

final userTokenProvider = StateProvider<String>((ref) => null);

final repositoryProvider = Provider((ref) => Repository(ref.read));

class Repository {
Repository(this.read);

/// `ref.read` 関数
final Reader read;

Future<Catalog> fetchCatalog() async {
String token = read(userTokenProvider);

final response = await dio.get('/path', queryParameters: {
'token': token,
});

return Catalog.fromJson(response.data);
}
}
注記

ref.read の代わりに ref をそのままオブジェクトに渡すこともできます。

final repositoryProvider = Provider((ref) => Repository(ref));

class Repository {
Repository(this.ref);

final Ref ref;
}

ただし、ref.read を渡す場合は ref に比べて若干コードの記述量を減らすことができ、 ref.watch が使用されないことを保証できるという利点があります。

【重要】 プロバイダ内で read は使わないでください。
final myProvider = Provider((ref) {
// ここで `read` を呼び出すのは悪いプラクティスです
final value = ref.read(anotherProvider);
});

プロバイダが無駄に更新されるのを避けたい場合は、 「プロバイダが頻繁に更新されます。どうしたらいいですか?」 の項目に目を通していただくことをおすすめします。

read をプロパティに持つオブジェクトのテスト方法を教えてください。

プロバイダ内で別のプロバイダの値を監視せずに、取得だけする方法はないですか?」 の項目で紹介したパターンを使う場合、オブジェクトのテストはどうやるんだろうと疑問に思うかもしれません。

この場合はオブジェクトではなく、それをラップするプロバイダの方をテストの対象とします。 これには ProviderContainer クラスを利用してください。

final repositoryProvider = Provider((ref) => Repository(ref.read));

test('fetches catalog', () async {
final container = ProviderContainer();
addTearDown(container.dispose);

Repository repository = container.read(repositoryProvider);

await expectLater(
repository.fetchCatalog(),
completion(Catalog()),
);
});

プロバイダが頻繁に更新されます。どうしたらいいですか?

プロバイダが頻繁に更新されると感じる場合は、もしかしたら依存先のプロバイダの値に、更新とは無関係な要素が含まれているかもしれません。

例えば、依存元プロバイダの値の評価に組み込まれる要素が Configuration オブジェクトの host プロパティのみだとします。 それにも関わらず、オブジェクト全体を監視している場合、host 以外のプロパティのみが変わった場合でもプロバイダは本来必要のない値の再評価を行います。

この問題を解決するには、Configuration の必要なプロパティのみを公開するプロバイダを別途作成してください。

【変更前】 オブジェクト全体を監視

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
// この場合、値の再評価に無関係なプロパティに変化があったとしても
// 製品リストを再取得する処理が実行されてしまう。
final configs = await ref.watch(configProvider.future);

return dio.get('${configs.host}/products');
});

【変更後】 select を利用して必要なプロパティのみを監視

final configProvider = StreamProvider<Configuration>(...);

final productsProvider = FutureProvider<List<Product>>((ref) async {
// select によりホスト名の値のみを監視することができる。
// Configuration の他のプロパティが変わっても無駄に値が再評価されることはない。
final host = await ref.watch(configProvider.selectAsync((config) => config.host));

return dio.get('$host/products');
});

これにより productsProviderhost の値が変わったときにのみ更新されるようになります。