Riverpodとは?Flutterの一番メジャーな状態管理を紹介!!

フロントシステムには、”状態管理”とても重要です。

以前紹介した「StatefulWidget」も状態管理を行う機能の1つですが、
複数の画面や機能を持つアプリを実装する場合、管理する状態も増えその管理が難しくなります。

そんな状態管理を簡単にできる、素敵なパッケージがRiverpodです。
今回は、Riverpodについて、説明します。

ここで紹介するRiverpodはv2です。

Riverpodとは

Riverpodとは、グローバルな状態(State)を定義するためのツールです。

変数や定数であれば、定義するスコープを変えることで、割と簡単に
グローバルな変数が定義できます。

しかし、フロントシステムで使われる状態(State)はWidgetと関係が深く、
状態の変化に合わせてWidgetも再構築されるため、そう簡単にはいきません。
さらに複数のWidgetで同じ状態情報を参照しようとするとさらに複雑になります。

それを簡単に実現できるツールが、Riverpodです。

Riverpodはグローバルな変数に使用するだけの、ツールではないのですが、
今回は基本的な説明にとどめています。

状態管理について

Riverpodの説明の前に、状態管理について説明します。

状態(State)

簡単に言うと、状態(State)とは画面上の情報に影響のある変数のことです。

例えば、ログインしているユーザー情報、ToDoリストの情報、
チェックボックスが選択有無、等々、これらの画面表示に影響があるデータが状態(State)です。

Widgetはこの状態を監視し、変化があるたびに画面を再構築します。

状態管理

それらの状態を適切に更新や削除をしたり、画面上に表現することを状態管理と言います。
状態にはローカルな状態とグローバルな状態があります。

  • ローカルな状態
    1つの画面や1つのWidgetにのみ使用される。
    その画面でしか使わない、テキストフィールドやカウンターの値など。
  • グローバルな状態
    アプリ全体で共有される状態。
    ホーム画面やユーザー情報画面に表示するユーザー情報など。

Flutterには状態管理のため”StatefulWidget”が用意されています。
しかし、”StatefulWidget”でグローバルな状態を管理しようとすると複雑になります。

他画面に状態を渡したり、他画面で更新される可能性がある場合、
その状態を使用する前に同期をとるなどの機能を実装する必要があるからです。

Riverpodでは、Providerが状態の監視を行っており、自動で影響のあるWidgetへ伝達されるため
上記のような同期機能を実装する必要はありません。

Riverpodについて

RiverpodはFlutterでの状態管理を容易にするために設計されたオープンソースのパッケージです。
以下の機能を用いて状態管理を行います。

  • Notifier
    Notifier(通知者)は対象となる状態や更新削除のロジックを管理するところです。
    状態を更新したり、状態の変更をウィジェットに通知する役割があります。
  • Provider
    WidgetはProvider(提供者)を使って、Notifier(状態)の値を参照したり、更新したりします。

Riverpodを使うと上記のイメージのようにWidgetAの更新がWidgetBへも伝わる!

Riverpodの使い方(概要)

Riverpodを使う際は以下を準備します。

  1. 状態クラス
  2. Notifier
  3. Provider

「3.Provider」についてはv2.0以降であれば、“Riverpod Generator”が自動で作成してくれます。

Riverpodの基本的な使い方

ToDoアプリを例に基本的な使い方をご紹介します。

設定ファイル

pubspec.yamlに以下のパッケージを追記します。

dependencies:
  flutter_riverpod:
  riverpod_annotation:
  freezed_annotation:

dev_dependencies:
  build_runner:
  riverpod_generator:
  

Riverpod関連のパッケージ
flutter_riverpod:FlutterでRiverpodを使うためのパッケージ
riverpod_generator:自動でRiverpodのProviderを定義してくれるパッケージ
riverpod_annotation:riverpod_generatorで使用するアノテーションを判定するパッケージ

その他のパッケージ
build_runner:実際にコードの自動生成を行うパッケージ
freezed:不変クラスを作成するためのパッケージ
freezed_annotation:freezedで使用するアノテーションを判定するパッケージ

状態のクラス定義

状態として利用するクラスは不変(Immutable)クラスである必要があります。
以下のようにfreezed等を利用して不変クラスを定義します。

import 'package:freezed_annotation/freezed_annotation.dart';

part 'todo.freezed.dart';

@freezed // freezedを使うためのアノテーション
class Todo with _$Todo {
  factory Todo({
    required String id,
    required String description,
    required bool completed,
  }) = _Todo;
}

不変であるメリットとは?

不変クラスとは、値の更新ができないクラスです。
つまり、1度作ったら値の変更ができません。なんとなく、不便そうですね。

非不変クラスのインスタンスのデータ受け渡しは”参照渡し”です。
他のクラスや関数にインスタンスを渡す場合は、値そのものではなく、
値が入っているアドレス値を渡します。

クラスや関数に引数として渡されたインスタンスの値を更新すると、
そのアドレスを参照している呼び出し元のインスタンスの値も変更されます。

これだけだと、状態管理に使えそうに思えますが、
参照渡しのやっかいなことは、読み取り用として渡したインスタンスであっても、
アドレス値の値を更新できてしまうことで、意図しない更新が発生する可能性があります。
また、この更新はエラーも発生しないため、検知できません。

このような意図しない更新は、例えば以下のようにコピーしたインスタンスをソートした場合、
コピー元も同じくソートされてしまいます。

Riverpodでは、上記のような意図しない変更を阻止するためにも、
意図した時にしか更新できないように不変クラスで状態を管理したいのです。

Notifier の定義

上記で定義したクラスをもとにNotifierを定義します。
ここでは、状態を更新や新規追加の処理を記述します。

import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'todo.dart';

// 以下手順にて、build runner を実行するとこのファイルが作成されます
part 'todo_notifier.g.dart';

@riverpod
class TodoNotifier extends _$TodoNotifier {
  @override
  List<Todo> build() {
    return [];
  }

  // 新規追加
  void addTodo(Todo todo) {
    state = [...state, todo];
  }

  // 削除
  void removeTodo(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id != todoId) todo,
    ];
  }

  // "completed"の判定切り替え
  void toggle(String todoId) {
    state = [
      for (final todo in state)
        if (todo.id == todoId)
          todo.copyWith(completed: !todo.completed)
        else
          todo,
    ];
  }
}

本記事はRiverpodの概要を説明するためのものなので、詳しい処理の説明は割愛します。

Riverpod Generatorの実行

以下コマンドをターミナルでbuild_runner実行します。

flutter pub run build_runner build --delete-conflicting-outputs

するとRiverpod Generatorが起動し、〜.d.dartファイルが作成されます。
上記コマンドでfreezedも起動するため、~.freezed.dartファイルも作成されます。

Riverpod Generatorにより、Providerが作成されます。
作成される〜.d.dartを見るとProviderが定義されていることが分かると思います。

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'todo_notifier.dart';

// **************************************************************************
// RiverpodGenerator
// **************************************************************************

String _$todoNotifierHash() => r'1330ee68f148d9ff18b1e0370e3f3541ab67c621';

@ProviderFor(TodoNotifier)
final todoNotifierProvider =
    AutoDisposeNotifierProvider<TodoNotifier, List<Todo>>.internal(
  TodoNotifier.new,
  name: r'todoNotifierProvider',
  debugGetCreateSourceHash:
      const bool.fromEnvironment('dart.vm.product') ? null : _$todoNotifierHash,
  dependencies: null,
  allTransitiveDependencies: null,
);

typedef _$TodoNotifier = AutoDisposeNotifier<List<Todo>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

Riverpodを使ったWidget

Riverpodを使うには、使う範囲を宣言する必要があります。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'todo_item.dart';
import 'todo_notifier.dart';

void main() {
  // Riverpodを使用するスコープを"ProviderScope"で宣言
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: TodoItemListPage(),
    );
  }
}

// "ConsumerWidget"でRiverpodを使うWidgetを定義
class TodoItemListPage extends ConsumerWidget {
  @override
  // "WidgetRef ref"はProviderを使用するためのコンテキスト
  Widget build(BuildContext context, WidgetRef ref) {
    // "ref.watch"はProviderを監視し、状態の変更を検知します。
    List<TodoItem> todoItems = ref.watch(todoNotifierProvider);

    return Scaffold(
      appBar: AppBar(title: Text('todoItem List')),
      body: ListView.builder(
        itemCount: todoItems.length,
        itemBuilder: (context, index) {
          final todoItem = todoItems[index];
          return ListTile(
            title: Text(todoItem.title),
            leading: Checkbox(
              value: todoItem.completed,
              onChanged: (bool? newValue) {
                // "ref.read"はProviderからNotifierのメソッドを呼び出しています。
                ref.read(todoNotifierProvider.notifier).toggle(todoItem.id);
              },
            ),
            onLongPress: () => ref.read(todoNotifierProvider.notifier).removeTodo(todoItem.id),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          // addTodo
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

“ref”はreference(参照)の略です。
Widgetから状態への参照は、ref→Provider→Notifier→Stateです。

※上記処理では、Notifierの”addTodo”は使用していません。

実行画面

実行すると以下のように、初期データが表示されます。
チェックボックスをタップしたり、リストアイテムを長押しすると、状態が変化します。

完成!!

最後に

今回はRiverpodの基本的なことについて、説明しました。
Riverpodは色々な機能があるため、今後はその機能についてもご紹介できればと思います。

本記事が少しでもRiverpodの理解の助けになれれば幸いです。
最後までお読みいただきありがとうございました!!

タイトルとURLをコピーしました