フロントシステムでは、”状態管理”がとても重要です。
以前紹介した「StatefulWidget」も状態管理を行うための機能ですが、
複数の画面や機能を持つアプリを実装する場合、管理する状態も増えその管理が難しくなります。
そんなときに状態を簡単に管理できる、素敵なパッケージが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を使う際は以下を準備します。
- 状態クラス
- Notifier
- 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の理解の助けになれれば幸いです。
最後までお読みいただきありがとうございました!!