【Flutter/Dart】Riverpodの使い方!状態管理の基本

【Flutter/Dart】Riverpodの使い方!状態管理の基本

この記事からわかること

  • Flutter/Dart状態管理を行うRiverpodとは?
  • StateProviderStateNotifierProviderScopeConsumerWidget使い方

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

環境

Riverpodとは?

公式リファレンス:https://riverpod.dev/

Riverpod」とはFlutterアプリで状態管理を行うためのパッケージです。従来人気だった「Provider」という状態管理のパッケージと同じ開発者が開発しており、ProviderのアナグラムでRiverpodと名付けられたようです。

Riverpod」を使用するメリットは「無駄な再描画(リビルド)を減らしパフォーマンスを向上できる」ことです。これはProviderでも同じ役割を果たすことが可能ですが、Widgetツリー(BuildContext)に依存してしまうデメリットがありました。これを解消しているのがRiverpodでProviderの上位互換的な位置付けになっています。

RiverpodとProviderの違い

Provider」との違いは以下の通りです。

項目 Provider Riverpod
状態の管理 Widgetツリーに依存 グローバルに定義可能(ツリーに依存しない)
BuildContext 依存 必須(watchやreadに必要) 不要(refで完結)
リスナーの扱い ChangeNotifierなどで明示的に実装 ref.watchで自動的に検知
再利用性 低め(Widgetツリー依存) 高い(他の層やテストで使いやすい)
グローバル状態管理 やや面倒(contextが必要) 簡単(refで直接アクセス可能)
テストのしやすさ 難しい(Widgetテストが必要) ProviderContainerで簡単にテスト可能

パッケージの種類

Riverpodに関するパッケージは3つ存在します。それぞれ使い道が異なりますがFlutterでiOS/Androidアプリ開発を行いたい場合はflutter_riverpodを使用すればOKです。

導入

pub.dev:flutter_riverpod

Flutterでflutter_riverpodを利用できるようにするために以下のコマンドを実行してパッケージを導入します。

$ flutter pub add flutter_riverpod

これでパッケージの導入が完了し、import文を追加すれば使用できるようになります。

import 'package:flutter_riverpod/flutter_riverpod.dart';

Providerの定義

Riverpodでも「Provider」という概念があります。これはProviderパッケージと基本的には同じ概念で値を提供するための仕組みを構築するためのものになります。Providerパッケージでは「状態を管理する機能」と「値を提供する機能(Provider)」が別のレイヤーとして管理されていました。

// 状態を管理する機能
class Counter extends ChangeNotifier {
  int count = 0;

  void increment() {
    count++;
    notifyListeners();
  }
}
// 値を提供する機能
final counterProvider = ChangeNotifierProvider((_) => Counter());

Riverpodパッケージでは状態管理とProviderが1セットとして定義することが可能です。

// 状態を直接もつ場合(StateProvider)
final counterProvider = StateProvider<int>((ref) => 0);

また別に定義することも可能ではあります。

// 状態をクラス化して使う場合(StateNotifier + StateNotifierProvider)
class Counter extends StateNotifier<int> {
  Counter() : super(0);
  void increment() => state++;
}

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

カウンターアプリで使い方を学ぼう

Riverpodの実装方法を理解するために簡単なカウンターアプリを作成してみます。その前に重要になるポイントを確認しておきます。それぞれの詳細は後述するとして全体のコードを確認してみましょう。

3つのポイント

状態を直接持つProvider

まずは状態を管理 & 値の変化を通知するProviderを定義します。StateProviderでは単一の値を保持します。

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

続いてProviderを使用できるようにするためにProviderScopeで囲みます。これはmainメソッド内のrunAppで囲んでおけばOKです。

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

最後に更新対象となるWidgetの継承をConsumerWidgetにします。StatelessWidgetを拡張したクラスでWidgetRef型の参照を取得することができ、ここからProviderの値を「監視(watch)」・「読み取り(read)」できるようになります。

class CounterRiverpodView extends ConsumerWidget {
  
  
  Widget build(BuildContext context, WidgetRef ref) {
    // 値の監視
    final count = ref.watch(counterProvider); 

    return Scaffold(
      appBar: AppBar(title: const Text('Counter with Riverpod')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text('Counter Value', style: TextStyle(fontSize: 20)),
            const SizedBox(height: 10),

            Text(
              '$count',
              style: const TextStyle(fontSize: 40, fontWeight: FontWeight.bold),
            ),

            const SizedBox(height: 20),

            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                FloatingActionButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).state--;
                  },
                  child: const Icon(Icons.remove),
                ),
                const SizedBox(width: 20),
                FloatingActionButton(
                  onPressed: () {
                    ref.read(counterProvider.notifier).state++;
                  },
                  child: const Icon(Icons.add),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Consumer系の種類

Riverpodでは登録したProviderへアクセスするためにWidgetRef型を介する必要があります。WidgetRef型を参照できるようにするためのConsumer系のクラスは色々あるので以下の記事を参考にしてください。

Providerの種類

Riverpodから提供されている「Provider」にはいくつか種類があります。

具体的なクラス 使い所
StateProvider 単一の値を管理したい場合
StateNotifierProvider 状態管理にロジックを含ませたい場合
Provider 結果をキャッシュさせたい場合
FutureProvider 非同期値(Future)の提供
StreamProvider リアルタイムなStreamデータの提供

StateProvider

公式リファレンス:StateProvider

StateProvider単一の値を管理するためのProviderです。 後述するStateNotifierProviderの簡易版になっており、データに変化があった場合に変更されたことを通知します。

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

扱えるデータ型

単一のデータ型であれば扱うことができるので状態管理に複雑な処理が絡まないのであればStateProviderを使用するのが一番シンプルです。

StateNotifierProvider

公式リファレンス:StateNotifierProvider

StateNotifierProviderStateNotifierクラスを監視し公開するためのProviderです。StateNotifierを継承したクラスに対して使用できるので状態管理だけでなく、ロジックを持たせたい場合に活用できます。

class Counter extends StateNotifier<int> {
  Counter() : super(0);
  
  /// 値をインクリメント
  void increment() {
    print('インクリメント');
    state++; 
    print('New value: $state');
  }

  /// 値をデクリメント
  void decrement() {
    print('デクリメント');
    state--; 
    print('New value: $state');
  }
}

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

Provider

公式リファレンス:Provider

Provider状態ではなく結果を保持するための読み取り専用のProviderです。今までは状態自体を保持し変化を外部へ通知することが役割でした。Providerを使用する場合としない場合をみながら使い方を考えてみます。今回は先ほどのカウンターの「デクリメントボタンに0の場合に非活性になる機能」を実装してみます。まずはProviderを使用しない場合です。


/// デクリメントボタン
/// この実装ではパフォーマンスに問題あり
class DecrementButton extends ConsumerWidget {
  const DecrementButton({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    /// デクリメント可能かどうか
    /// watchで常に変化を観測し、変化するたびに再描画
    final canDecrement = ref.watch(counterProvider) > 0;
    /// デクリメント
    void decrement() {
      ref.read(counterProvider.notifier).update((state) => state - 1);
    }
    /// ボタンUI
    return ElevatedButton(
      onPressed: canDecrement ? decrement : null,
      child: const Icon(Icons.remove),
    );
  }
}

機能的には問題はなく動作しますがパフォーマンス的に問題があります。それはcounterProvider自体をwatchしているためカウンターの値が変化するたびにデクリメントボタン自体が都度再描画されてしまう事です。本来なら活性 / 非活性が切り替わるタイミングの時のみ再描画されることが理想です。これがProviderを使用することで実現できます。「インクリメント可能かどうかを計算するプロバイダ」を作成し、カウンターの観測結果のみを公開するようにすることでカウンターの値の変化での再描画ではなく、インクリメント可能かどうかの結果の変化で再描画されるようにできました。


/// インクリメント可能かどうかを計算するプロバイダ
final canIncrementProvider = Provider<bool>((ref) {
  return ref.watch(counterProvider) != MAX_COUNT;
});

/// インクリメントボタン
/// [canIncrementProvider]に検知ロジックを切り離す
class IncrementButton extends ConsumerWidget {
  const IncrementButton({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    /// インクリメント可能かどうか
    /// watchで常に変化を観測し、変化するたびに再描画
    final canIncrement = ref.watch(canIncrementProvider);
    /// インクリメント
    void increment() {
      ref.read(counterProvider.notifier).update((state) => state + 1);
    }
    /// ボタンUI
    return ElevatedButton(
      onPressed: canIncrement ? increment : null,
      child: const Icon(Icons.add),
    );
  }
}

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article