🌟

【Riverpod】プロバイダーの読み取りのref.xxxを整理して解説

に公開

はじめに

皆さん、こんにちは。

Riverpodを学ぶ上で、refを介したプロバイダーの読み取りは最も重要な概念です。しかし、ref.watchref.readref.listen、そしてselectといった複数のメソッドがあり、それぞれの使い分けに戸惑うこともあるでしょう。この記事では、公式ドキュメントの内容を基に、これらのメソッドの役割と正しい使い方を具体的なコード例を交えて解説します。

サンプルコード

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

ConsumerWidgetを継承してref を使えるように

概要

  • Providerを読み取るウィジェットはConsumerWidgetを継承して
  • buildメソッドのWidgetRef型引数(ref)を利用する
  • Providerの読み取り以外はStatelessWidgetと同じ
  • Flutter Riverpod Snippetsでは「stlessConsumer」で雛形

プロバイダーの値を読み取るウィジェットを作成するには、ConsumerWidgetを継承するのが一般的な方法です。

ConsumerWidgetを継承するとbuildメソッドの引数としてWidgetRef型のrefを受け取ります。このrefを通じて、プロバイダーの値をref.watch()ref.read()ref.listen()などのメソッドを使います。

ConsumerWidgetは、プロバイダーの読み取り機能を除けば、StatelessWidgetとほとんど同じように記述できます。

Flutter Riverpod Snippets(VS Code拡張機能)を使っている場合は、「stlessConsumer」と入力するだけで、ConsumerWidgetの基本的な雛形が自動生成されます。

例(雛形のまま)

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // buildメソッド内でrefを使ってProvierを読み取る
    return Container();
  }
}

https://riverpod.dev/ja/docs/concepts/reading#ウィジェットから-ref-を取得する

ref.readでその場限りの利用

概要

  • 最もシンプルな読み取り方法
  • 現在の状態を一度だけ取得し更新を監視しない
    • 「その場限り」での利用に収めること
    • buildメソッドの直下では利用しないこと
  • UIに反映させる必要のないデータを読み取る
  • 主にNotifier(更新処理の呼び出し)の参照で利用する

ref.readはプロバイダーの最もシンプルな読み取り方法です。readでの読み取りはプロバイダーの現在の状態を一度だけ取得し、その後の更新を監視しません。

この特性から、ref.readは「その場限り」での利用に留めるべきです。例えば、ボタンのonPressedコールバック内で、その瞬間のデータ値(状態に保存したユーザ名とパスワード等)を取得したり、Notifierのインスタンスを参照して更新処理を呼び出す際に主に利用します。

ElevatedButtonで状態更新処理を行う例

ElevatedButton(
  onPressed: () {
    // 状態更新をメソッドチェーンで
    ref.read(counterProvider.notifier).increment();
  },
  child: Text('+'),
),

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // このようにbuildの直下でのreadはアンチパターン
    // final currentCount = ref.read(counterProvider);
    // final counter = ref.read(counterProvider.notifier);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text('$counter'),
            SizedBox(width: 10),
            ElevatedButton(
              onPressed: () {
                // 処理内での一次的な値の利用なので推奨される書き方
                final currentCount = ref.read(counterProvider);
                // 更新1:状態更新の呼び出しなので推奨される書き方
                final counter = ref.read(counterProvider.notifier);
                counter.increment();
                // 更新2:状態更新はメソッドチェーンの以下でもOK
                ref.read(counterProvider.notifier).increment();
              },
              child: Text('+'),
            ),
          ],
        ),
      ],
    );
  }
}

https://riverpod.dev/ja/docs/concepts/reading#refread-を使ってプロバイダのステートを取得する

ref.watchで変更を監視してUIに反映

概要

  • プロバイダーを読み取りリアクティブな値(状態)を取得
  • 状態の変更を監視し、変更に伴いUIを自動更新
  • 値(状態)の読み取りはできるだけwatch を使う

ref.watchは、最も基本的でかつ推奨される読み取り方法です。ref.watchは単に現在の値を取得するだけでなく、状態の変更を監視し、変更に伴ってUIを自動的に更新する「リアクティブな値」を取得します。

例えば、カウンターアプリの画面に現在の数字を表示する場合、ref.watchを使ってカウンターのプロバイダーを監視します。カウンターの値が増えるたびに、ref.watchがその変更を検知し、カウンターの数字を表示しているウィジェットだけを自動的に再構築してくれるため、常に最新の数字が画面に反映されます。

このため、UIに表示するデータや、UIの振る舞いを決定するような状態を扱う際は、できるだけref.watchを使うことが推奨されます。

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // 状態の読み取り
    final counter = ref.watch(counterProvider);
    final hello = ref.watch(helloProvider);
    // このように静的な値でもreadで読み取るのはアンチパターン
    // final hello = ref.read(helloProvider);

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        // 値の利用は通常の変数と同じ
        Text('$counter'),
        Text(hello),
      ],
    );
  }
}

https://riverpod.dev/ja/docs/concepts/reading#refwatch-を使ってプロバイダを監視する

ref.listenで状態更新に伴う副作用

概要

  • 状態更新を監視し、任意の関数を実行
  • 更新前の値と更新後の値を利用可能
  • 状態更新をきっかけにした副作用処理で利用
    • 画面遷移、スナックバー表示、など

ref.listenは、状態更新に伴う副作用を処理するためのメソッドです。プロバイダーの状態更新を監視し、状態が変化するたびにUIの再構築を伴わずに任意の関数を実行できます。

コールバック内では、previous(更新前の値)とnext(更新後の値)の両方を利用することができます。ref.listenの主な用途は、状態更新をきっかけにした副作用処理です。

例えば、ユーザーの認証状態が「ログイン済み」に変わった際に画面遷移を行ったり、フォームの送信結果(成功/失敗)に応じて「保存しました」といったスナックバー表示やエラーダイアログを表示したりなどです。UIとは直接関係ないログ出力やアナリティクスイベントの送信などにも利用できます。

watchがUIの再構築を伴うのに対し、listenはUIとは独立した副作用を実行するために使われます。これにより、UIの描画ロジックと、それに付随する非UI的な処理を明確に分離することができます。

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // 状態の読み取り
    final counter = ref.watch(counterProvider);

    // Counterが更新されたら実行したい関数を登録
    ref.listen(counterProvider, (int? pre, int next) {
      print('$pre -> $next changed!');
    });

    return Column( 省略 );
  }
}

https://riverpod.dev/ja/docs/concepts/reading#reflisten-を使ってプロバイダを監視する

プロバイダー.select で状態の一部のみを監視

概要

  • watchlistenの引数をプロバイダー.select(監視対象の指定) にする
  • オブジェクトの一部のプロパティのみを監視対象にしたい時に利用
    • オブジェクト全体を監視すると無関係な変更で不必要なUIの再構築が発動してしまう
  • パフォーマンス最適化の観点で利用する機能

状態がオブジェクトの場合はref.watch()ref.listen()の引数をプロバイダー.select() とすることで、状態の一部のみを監視対象とすることができます。

オブジェクトを状態としている場合、ref.watch()でそのまま監視すると、無関係なプロパティが変更されただけでも不必要なUIの再構築が発動してしまいます。例えば、ユーザーオブジェクトのageが変わっただけで、nameしか表示していないウィジェットが再構築されるといった事態です。

これを避けるために、ref.watch()ref.listen()の引数に、provider.select((state) => state.property)のように監視したいプロパティを返す関数を指定します。これにより、指定したプロパティの値が変更されたときだけウィジェットが更新されるようになります。

selectは、無駄な再構築を防ぎ、アプリケーションのパフォーマンスを最適化する観点で重要な機能です。

例(Userオブジェクトのnameだけを監視するウィジェット)

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

  
  Widget build(BuildContext context, WidgetRef ref) {
    // Userオブジェクトのnameだけを監視対象にする
    final userName = ref.watch(
      userNotifierProvider.select((user) => user.name),
    );

    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            // Userオブジェクトのnameを利用
            Text(userName),
            ElevatedButton(
              onPressed: () {
               // 状態の一部を更新する処理の呼び出し
                ref.read(userNotifierProvider.notifier).updateState(newName: '新しい名前');
              },
              child: Text('名前変更'),
            ),
          ],
        ),
      ],
    );
  }
}

例(プロバイダーとUser型の定義)

import 'package:riverpod_annotation/riverpod_annotation.dart';

part 'user.g.dart';


class UserNotifier extends _$UserNotifier {
  
  User build() {
    return User(name: 'John', age: 10);
  }

  // 状態の更新
  void updateState({String? newName, int? newAge}) {
    state = state.copyWith(name: newName, age: newAge);
  }
}

// User型
class User {
  String name;
  int age;

  User({required this.name, required this.age});

  // 渡された引数の項目のみ更新した新しいオブジェクトを生成
  User copyWith({String? name, int? age}) {
    return User(name: name ?? this.name, age: age ?? this.age);
  }
}

https://riverpod.dev/ja/docs/concepts/reading#selectを使って更新の条件を限定する

おわりに

この記事では、Riverpodのrefを介したプロバイダーの読み取り方法について、それぞれの役割と使い分けを詳細に解説しました。

  • ref.read: UIに直接反映させる必要のない、その場限りのデータ取得やメソッド呼び出しに使います。buildメソッド直下での使用はアンチパターンです。
  • ref.watch: 状態の変化を監視し、UIを自動で更新するための、最も基本的なリアクティブな読み取り方法です。UIに表示するデータには基本的にwatchを使用します。
  • ref.listen: 状態変化をトリガーに、UIの再構築を伴わない副作用(画面遷移やスナックバー表示など)を実行するために使います。
  • select: オブジェクトの状態の一部のみを監視対象に絞り込み、不要な再構築を防ぐことで、パフォーマンスを最適化します。

これらのメソッドは、それぞれ異なる目的とユースケースを持っています。使い分けを理解し実践することで、Riverpodの持つ強力なリアクティブ性を最大限に活かし、クリーンでパフォーマンスの高いアプリケーションを構築できるようになります。

Discussion