メインコンテンツに進む

プロバイダの利用方法

本セクションの前に「プロバイダとは」のセクションに目を通していただくことをおすすめします。

ここではプロバイダの利用方法について説明します。

ref オブジェクトを取得する

プロバイダを利用するには、まず ref オブジェクトを取得する必要があります。

このオブジェクトを通じて、プロバイダと様々なやり取りを行うことになります。 ref はウィジェットもしくはプロバイダから取得することができます。

プロバイダから ref を取得する

プロバイダはすべて ref オブジェクトを引数として受け取ります。

final provider = Provider((ref) {
// `ref` を通じて他のプロバイダを利用する
final repository = ref.watch(repositoryProvider);

return SomeValue(repository);
})

この ref はさらに別のオブジェクトに渡すこともできます。

例えば...

final counterProvider = StateNotifierProvider<Counter, int>((ref) {
return Counter(ref);
});

class Counter extends StateNotifier<int> {
Counter(this.ref) : super(0);

final Ref ref;

void increment() {
// Counter は `ref` を使って他のプロバイダーを利用することができる
final repository = ref.read(repositoryProvider);
repository.post('...');
}
}

これで StateNotifier である Counterref を通じて他のプロバイダを利用することができるようになりました。

ウィジェットから ref を取得する

通常のウィジェットでは ref を使用することができないため、Riverpod では複数の方法を用意しています。

StatelessWidget の代わりに ConsumerWidget を継承する

ウィジェットツリーの中で ref を取得するには StatelessWidgetConsumerWidget に置き換えるのが基本です。

ConsumerWidgetStatelessWidget とほぼ同等のものであり、build メソッドに第2パラメータが存在する以外の違いはありません。 それが ref です。

ConsumerWidget の使用例は次の通りです。

class HomeView extends ConsumerWidget {
const HomeView({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// `ref` を使ってプロバイダーを監視する
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

StatefulWidget+State の代わりに ConsumerStatefulWidget+ConsumerState を継承する

ConsumerWidget と同様、ConsumerStatefulWidget+ConsumerStateStatefulWidget+State に対応します。 唯一の違いは ref オブジェクトが使用できるということです。

ただ ConsumerWidget と異なり、build メソッドに ref オブジェクトは渡されていません。 refConsumerState のプロパティです。

class HomeView extends ConsumerStatefulWidget {
const HomeView({Key? key}) : super(key: key);

@override
HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// `ref` は StatefulWidget のすべてのライフサイクルメソッド内で使用可能です。
ref.read(counterProvider);
}

@override
Widget build(BuildContext context) {
// `ref` は build メソッド内で使用することもできます。
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

HookWidget の代わりに HookConsumerWidget を継承する

これは flutter_hooks のユーザ向けです。 フックを使用するために HookWidget を使っている場合、そのまま ConsumerWidget に置き換えることができません。

そこで登場するのが hooks_riverpodHookConsumerWidget です。 HookConsumerWidgetConsumerWidgetHookWidget の役割を併せ持ちます。 これにより、ウィジェットはプロバイダーとフックの両方を利用することができます。

使用例は次の通りです。

class HomeView extends HookConsumerWidget {
const HomeView({Key? key}) : super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// build メソッド内でフックを使用できます。
final state = useState(0);

// `ref` オブジェクトを使ってプロバイダを監視することもできます。
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

HookWidget の代わりに StatefulHookConsumerWidget を継承する

flutter_hooks のフックに加えて StatefulWidget のライフサイクルメソッドを活用する必要がある場合は StatefulHookConsumerWidget に置き換えてください。

使用例は次の通りです。

class HomeView extends StatefulHookConsumerWidget {
const HomeView({Key? key}) : super(key: key);

@override
HomeViewState createState() => HomeViewState();
}

class HomeViewState extends ConsumerState<HomeView> {
@override
void initState() {
super.initState();
// 「ref」は StatefulWidget のすべてのライフサイクルメソッド内で使用できます。
ref.read(counterProvider);
}

@override
Widget build(BuildContext context) {
// HookConsumerWidget のように build メソッドの中でフックが使えます。
final state = useState(0);

// プロバイダ監視のために「ref」を使用することも可能です。
final counter = ref.watch(counterProvider);
return Text('$counter');
}
}

Consumer と HookConsumer ウィジェット

ref オブジェクトは Consumer もしくは HookConsumer ウィジェットのコールバック関数 builder から取得することもできます。

これらのウィジェットを使用する場合は ConsumerWidget および HookConsumerWidget のようにクラスを定義する必要がありません。 使用例は次の通りです。

Scaffold(
body: HookConsumer(
builder: (context, ref, child) {
// HookConsumerWidget と同様にフックが使えます。
final state = useState(0);

// `ref` オブジェクトを使ってプロバイダを監視することもできます。
final counter = ref.watch(counterProvider);
return Text('$counter');
},
),
);

ref を使ってプロバイダを利用する

ref オブジェクトの取得方法が分かったところで、早速使ってみましょう。

ref の使い道は主に3通りあります。

  • ref.watch: プロバイダの値を取得した上で、その変化を監視する。値が変化すると、その値に依存するウィジェットやプロバイダの更新が行われる。
  • ref.listen: プロバイダの値を監視し、値が変化するたびに呼び出されるコールバック関数(画面遷移、ダイアログの表示など)を登録する。
  • ref.read: プロバイダの値を取得する(監視はしない)。クリックイベント等の発生時に、その時点での値を取得する場合に使用できる。
note

機能の実装時には可能な限り ref.watch を使用するようにしてください。 ref.watch によりアプリはリアクティブかつ宣言的になり、コードの保守性を高めることができます。

ref.watch を使ってプロバイダを監視する

ウィジェットあるいはプロバイダ内で ref.watch を使ってプロバイダを監視することができます。

例えば、ref.watch でプロバイダに別の複数のプロバイダを監視させ、それらの値を組み合わせて新たに値を生成するということも可能です。

この手法は Todo リストのフィルタリングにも活用できます。 例えば、Todo アプリに次のプロバイダがあるとします。

  • filterTypeProvider: 現在のフィルタの種類を公開するプロバイダ(なし、完了のみ表示、未完了のみ表示...)
  • todosProvider: Todo リストの内容をすべて公開するプロバイダ

これらのプロバイダを第3のプロバイダで ref.watch を使って組み合わせ、Todo リストにフィルタを適用します。

final filterTypeProvider = StateProvider<FilterType>((ref) => FilterType.none);
final todosProvider = StateNotifierProvider<TodoList, List<Todo>>((ref) => TodoList());

final filteredTodoListProvider = Provider((ref) {
// フィルタの種類と Todo リストを取得、監視する
final FilterType filter = ref.watch(filterTypeProvider);
final List<Todo> todos = ref.watch(todosProvider);

switch (filter) {
case FilterType.completed:
// Todo リストを完了タスクのみにフィルタリングして値を返す
return todos.where((todo) => todo.isCompleted).toList();
case FilterType.none:
// フィルタ未適用の Todo リストをそのまま返す
return todos;
}
});

これで filteredTodoListProvider はフィルタ適用済みの Todo リストを公開することができます。

この Todo リストは、filterTypeProvidertodosProvider の値が変わると自動的に更新されます。 逆に言えば、いずれかが変わらない限りは再計算されることはありません。

同様にウィジェット内でも ref.watch を使ってプロバイダの値を表示し、値が変わるたびに UI を更新させることができます。

final counterProvider = StateProvider((ref) => 0);

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
// `ref` を使ってプロバイダを監視する
final counter = ref.watch(counterProvider);

return Text('$counter');
}
}

このサンプルコードでは、カウンターの数字を値として管理するプロバイダを監視しています。 プロバイダの値が変わると、ウィジェットが更新されて新しい値で UI が構築されます。

caution

watch メソッドは ElevatedButtononPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。

これらの場合は代わりに ref.read を使用してください。

ref.listen を使ってプロバイダを監視する

ref.listenref.watch と同様にプロバイダを監視することができます。

最も大きな違いは、ref.watch が値の変化に応じてウィジェットやプロバイダを更新するのに対して、 ref.listen は任意の関数を呼び出してくれるという点です。

例えば、エラー発生時のスナックバー表示など、何かしらの変化に反応して処理を実行したいときに便利です。

ref.listen メソッドは2つの位置引数が必要です。 第1引数にプロバイダ、第2引数にステート(状態)が変化した際に実行するコールバック関数を設定します。 このコールバック関数には呼び出し時に、プロバイダの直前のステートと新しいステートの値が渡されるため、それぞれをパラメータとして使用できます。

以下はプロバイダ内で使用した例です。

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));

final anotherProvider = Provider((ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});
// ...
});

また、ウィジェットの build メソッド内でも使用できます。

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
ref.listen<int>(counterProvider, (int? previousCount, int newCount) {
print('The counter changed $newCount');
});

return Container();
}
}
caution

listen メソッドは ElevatedButtononPressed 内など、非同期的な場面で呼び出さないでください。 また initState を始め、State のライフサイクルメソッド内での使用も避けてください。

ref.read を使ってプロバイダのステートを取得する

ref.read メソッドを使うことでプロバイダのその時点でのステートを取得することができます。純粋な取得以外の副作用はありません。

ref.read はユーザ操作によって呼び出される関数内で使用するのが一般的です。 例えば、ボタンクリックイベント発生時にカウンターの数字を変更する場合に使用できます。

final counterProvider = StateNotifierProvider<Counter, int>((ref) => Counter(ref));

class HomeView extends ConsumerWidget {
const HomeView({Key? key}): super(key: key);

@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
floatingActionButton: FloatingActionButton(
onPressed: () {
// `Counter` クラスの `increment()` メソッドを呼び出す
ref.read(counterProvider.notifier).increment();
},
),
);
}
}
note

ref.read はリアクティブではないため、可能な限り使用を避けてください。

watchlisten の使用では問題が生じる場合の回避策として存在しています。 ほとんどの場面では watchlisten の使用、特に watch の使用がベターなはずです。

【重要】 ref.read は build メソッドの中で使わない

ウィジェットのパフォーマンスを最適化するため、ref.read を次のように使いたくなることがあるかもしれません。

final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
// `ref.read` を使うことでプロバイダのステート変化は無視される
final counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

しかしこれは悪い使用例であり、追跡困難なバグの要因になり得ます。

このような ref.read の使い方は、次のような考え方がもとになっていることが多いと思われます。 「プロバイダが公開する値が変わることはない。だから ref.read の使用は安全である」。 しかし、そのプロバイダは今は値が変わることがないかもしれませんが、明日も同じことが言えるとは保証できません。 その点でこの推測には問題があると言えます。

ソフトウェアに変更はつきものです。そして将来、絶対に変わらないと考えられていた値が、変わる必要に迫られることは十分あり得ることです。 そしてもしこの値の取得に ref.read が使われていたら、その時にはコードを振り返り ref.read が使われているところをすべて ref.watch に変更する必要があります。これはエラーを招くおそれがありますし、変更し忘れる箇所がいくつか出てくる可能性もあります。

一方、最初から ref.watch を使用していれば、リファクタリング時に生じる問題は比較的少ないはずです。

でもウィジェットの更新回数を抑えるためにどうしても ref.read が使いたい

このような声に対しては、ref.watch を使用してまったく同じ効果を得ることができる点を強調しておきたいです。

例えば...

final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.read(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

とする代わりに、

final counterProvider = StateProvider((ref) => 0);

Widget build(BuildContext context, WidgetRef ref) {
StateController<int> counter = ref.watch(counterProvider.notifier);
return ElevatedButton(
onPressed: () => counter.state++,
child: const Text('button'),
);
}

としてください。 この2つのコードの効果はいずれも同等です。 カウンターの数字が増えた際にボタンが更新されることはありません。

加えて、後者のアプローチの場合はカウンターの数字がリセットされた場合にも対応することができます。 例えば、別の場所で次のメソッドを呼び出すとします。

ref.refresh(counterProvider);

するとこの ref.refresh メソッドにより StateController オブジェクトは再度生成されます。

もしここで ref.read を使用していたら、ボタンは古い StateController インスタンスを使うことになります。 このインスタンスはこの時点で既に破棄されているため、使うべきではないはずです。 一方 ref.watch を使用した場合は、ボタンが更新されて新しい StateController を取得することができます。

プロバイダから取得できる値の種類

利用するプロバイダによっては、取得できる値の種類にバリエーションが存在します。

例えば、次のような StreamProvider があるとします。

final userProvider = StreamProvider<User>(...);

この userProvider を利用する場合は、以下の値を取得することができます。

  • userProvider を監視することで、Stream の現在のステートを同期的に取得する。

    Widget build(BuildContext context, WidgetRef ref) {
    AsyncValue<User> user = ref.watch(userProvider);

    return user.when(
    loading: () => const CircularProgressIndicator(),
    error: (error, stack) => const Text('Oops'),
    data: (user) => Text(user.name),
    );
    }
  • userProvider.stream を監視することで、Stream を取得する。

    Widget build(BuildContext context, WidgetRef ref) {
    Stream<User> user = ref.watch(userProvider.stream);
    }
  • userProvider.future を監視することで、Stream から出力された最新の値で解決する Future を取得する。

    Widget build(BuildContext context, WidgetRef ref) {
    Future<User> user = ref.watch(userProvider.future);
    }

その他のプロバイダで取得できる値の種類については、 API reference を参照してください。

selectを使って更新の条件を限定する

最後に、ウィジェットおよびプロバイダの更新(もしくは ref.listen による関数の実行)の条件を限定する方法をご紹介します。

プロバイダを監視するということは、そのプロバイダが公開するオブジェクト全体のステートを監視するということです。 しかし、その監視の範囲を狭めて特定のプロパティのみを監視対象としたい場合があります。

例えば、次の User オブジェクトを公開するプロバイダがあるとします。

abstract class User {
String get name;
int get age;
}

それを監視するウィジェット側は user の name プロパティしか使用しません。

Widget build(BuildContext context, WidgetRef ref) {
User user = ref.watch(userProvider);
return Text(user.name);
}

このように userProvider を普通に監視すると、 無関係な age プロパティの変化もウィジェット更新のトリガーとなってしまいます。

監視対象を name プロパティに限定したい場合は、select を使ってその旨明示します。

変更後のコードは次のようになります。

Widget build(BuildContext context, WidgetRef ref) {
String name = ref.watch(userProvider.select((user) => user.name));
return Text(name);
}

つまり select を使って監視対象にするプロパティを返す関数を指定することができるのです。

これで User オブジェクトに変化があるたびに、Riverpod はこの関数を呼び出し、対象プロパティの古い値と新しい値を比較します。 そして値が異なる場合は Riverpod はウィジェットを更新します。 一方、対象プロパティ以外の値が変わっただけで対象プロパティの値自体が不変の場合は、Riverpod はウィジェットの更新を行いません。

info

もちろん、selectref.listen を組み合わせることもできます。

ref.listen<String>(
userProvider.select((user) => user.name),
(String? previousName, String newName) {
print('The user name changed $newName');
}
);

このようにすることで name プロパティの値が変わったときにだけ、第2引数で登録したコールバック関数を呼び出すことが可能になります。

tip

select で明示する値は、必ずしも対象オブジェクトのプロパティそのものである必要はありません。 == 演算子のオーバーライドなどでオブジェクトの等価性が定義されていれば何を返しても問題ありません。 例えば、以下のような値を返すことも可能です。

final label = ref.watch(userProvider.select((user) => 'Mr ${user.name}'));