Flutterではほぼ必須!状態管理パッケージRiverpodの紹介
riverpodとは
Riverpodは、Flutterアプリケーションにおける状態管理のためのフレームワークで、Providerを進化させたものです。Remi Rousselet氏によって開発され、よりテストしやすく、柔軟で、スケーラブルな状態管理が可能になっています。
Riverpodはコンパイル時の安全性を重視し、エラーを減らし、依存性の注入を容易にします。また、アプリケーション全体で共有されるグローバルな状態管理が可能で、UIとロジックの分離を促進します。
riverpodを使うことのメリット
- 型安全性: コンパイル時のエラーチェックにより、実行時のエラーを削減。
- 再利用性: ロジックと状態の再利用が容易になる。
- スコープ管理: 状態のスコープが明確で管理しやすい。
- テストしやすさ: 単体テストが書きやすい。
riverpodの種類と使うべき場面
- Provider: 不変の値やオブジェクトを提供する際に使用。例えば、アプリの設定やテーマ情報など、変更されることのないデータの提供に適しています。
- StateProvider: 単純な状態を持つ値の管理に使用。UIの状態や小規模なデータセットの管理に最適で、状態が更新されるとウィジェットが再構築されます。
- StateNotifierProvider: 状態管理のためにStateNotifierクラスを使用し、複雑な状態や複数の値を効率的に管理します。
- FutureProvider: 非同期処理から得られる単一の値を管理。例えば、ネットワークリクエストから取得したデータの表示に使用されます。
- StreamProvider: 継続的に値が変化するストリームを監視。リアルタイムで更新されるデータフィードやイベントの監視に適しています。
-
ChangeNotifierProvider: 複雑な状態ロジックを含むオブジェクトの管理。Flutterの
ChangeNotifier
クラスを使用して状態の変更を通知します。
riverpodでの基本的な状態管理方法
Riverpodの状態管理を詳しく説明するために、StateProvider
を用いたカウンターアプリケーションの例を拡張します。
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// StateProviderを定義して、初期値として0をセットします。
final counterProvider = StateProvider<int>((ref) => 0);
void main() {
runApp(
ProviderScope(
child: MyApp(),
),
);
}
class MyApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// counterProviderを監視して、その値をUIに表示します。
final int count = ref.watch(counterProvider);
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Riverpod Counter Example')),
body: Center(
child: Text('Counter: $count'),
),
floatingActionButton: FloatingActionButton(
// ボタンが押されたときにcounterProviderの状態を更新します。
onPressed: () => ref.read(counterProvider.notifier).state++,
child: Icon(Icons.add),
),
),
);
}
}
このコードでは、StateProvider
を使用して整数値の状態を管理しています。ProviderScope
ウィジェットでアプリケーションをラップすることで、状態がアプリケーション全体で利用可能になります。 ref.watch
を用いて状態を監視し、状態が更新されるとウィジェットが再描画されます。
FloatingActionButton
を押すことで、状態がインクリメントされ、UIが更新されます。このパターンにより、状態管理が直感的で効率的に行えます。
ちょっと応用:StateNotifierProviderを使ってTodoアプリを作る
ではちょっと応用で、StateNotifierProvider
を使ってTodoアプリを作成してみます!
※ riverpodだけではなく、他パッケージを使って開発している箇所もあります。
1. Todoエンティティの定義
todo_riverpod/entities/todo_item.dart
で、Todoアイテムのデータ構造を表すTodoItem
エンティティを定義します。
ここではfreezedというパッケージを使って、コードを自動生成しています。
import 'package:freezed_annotation/freezed_annotation.dart';
part 'todo_item.freezed.dart';
part 'todo_item.g.dart';
class TodoItem with _$TodoItem {
factory TodoItem({
required int todoId,
('') String title,
('') String content,
}) = _TodoItem;
factory TodoItem.fromJson(Map<String, dynamic> json) =>
_$TodoItemFromJson(json);
}
2. ローカルデータリポジトリの実装
todo_riverpod/external_interface/repositories/local_data_repository_impl.dart
で、TodoアイテムのCRUD操作を行うLocalDataRepositoryImpl
を実装します。
ローカルデータベースには、SharedPreferenceというパッケージを使用しています。
-
fetchTodoList()
: ローカルストレージからTodoアイテムのリストを取得 -
saveTodoItem(String title)
: 新しいTodoアイテムを保存 -
updateTodoItem(int todoId, String title, String content)
: 既存のTodoアイテムを更新 -
deleteTodoItem(int todoId)
: Todoアイテムを削除
3 StateNotifierProviderの作成
todoListProvider
をStateNotifierProvider
として定義し、Todoリストの状態を管理します。
このProviderをUI側で監視して、TodoのCRUD操作を行うメソッドを呼び出したり、Todoリストを取得します。
final todoListProvider = StateNotifierProvider<TodoListNotifier, List<TodoItem>>((ref) {
final localDataRepositoryImpl = ref.read(localDataRepositoryProvider);
return TodoListNotifier(localDataRepositoryImpl: localDataRepositoryImpl);
});
4. TodoListNotifierの実装
TodoListNotifier
はStateNotifier<List<TodoItem>>
を継承し、Todoリストの状態を管理します:
-
fetchTodoList()
: 非同期でTodoリストを取得し、状態を更新 -
saveTodoItem({required String title})
: 新しいTodoアイテムを保存し、リストを更新 -
updateTodoItem(...)
: 既存のTodoアイテムを更新し、リストを更新 -
deleteTodoItem(...)
: Todoアイテムを削除し、リストを更新
final todoListProvider =
StateNotifierProvider<TodoListNotifier, List<TodoItem>>(
...
);
class TodoListNotifier extends StateNotifier<List<TodoItem>> {
TodoListNotifier({required this.localDataRepositoryImpl}) : super([]);
final LocalDataRepositoryImpl localDataRepositoryImpl;
Future<void> fetchTodoList() async {
final todoList = await localDataRepositoryImpl.fetchTodoList();
state = todoList;
}
void saveTodoItem({required String title}) {
localDataRepositoryImpl.saveTodoItem(title: title);
fetchTodoList();
}
void updateTodoItem({
required int todoId,
required String title,
required String content,
}) {
localDataRepositoryImpl.updateTodoItem(
todoId: todoId, title: title, content: content);
fetchTodoList();
}
void deleteTodoItem({
required int todoId,
}) {
localDataRepositoryImpl.deleteTodoItem(todoId: todoId);
fetchTodoList();
}
}
5. ページ側でのProviderの使用
FlutterアプリでtodoListProvider
を使用してTodoリストの状態を管理します。
-
ref.read(todoListProvider)
を使用して保存、更新、削除などのアクションを実行します。 -
ref.watch(todoListProvider)
を使用してTodoリストの状態を監視し、変更時にUIを再構築します。
class TodoListPage extends HookConsumerWidget {
const TodoListPage({super.key});
Widget build(BuildContext context, WidgetRef ref) {
// TodoリストのStateに変更があるかどうか監視することで、UIに変更をすぐに反映することができる
final todoList = ref.watch(todoListProvider);
useEffect(() {
// アプリ起動時に、ローカルDBに保存されているTodoリストを取得してくる
ref.read(todoListProvider.notifier).fetchTodoList();
return null;
}, []);
return Scaffold(...);
...
6. UIの実装
今回は割愛しますが、ListView
などのウィジェットを使用してTodoアイテムを表示していきます。
上のコードをそのまま使用した場合、todoList[index]とすることで、一つひとつのTodoを表示することができると思います。
Todoアイテムを追加、編集、削除するときも、ボタンを押したときのFunctionとして、ref.read(todoListProvider.notifier).deleteTodoItem(todoId: todoList[index].todoId);のように呼び出してあげるとTodoアプリの完成です!
まとめ
Riverpodは、Flutterの状態管理をより効率的かつ柔軟にするための強力なパッケージです。
型安全性、スコープの明確化、依存性の容易な注入により、開発者はアプリケーションをより簡単に管理し、スケールアップできます。適切なProviderを選択し、基本的な状態管理の実装を理解することで、かなりアプリ開発において助けになると思います!
Discussion