📱

Flutterではほぼ必須!状態管理パッケージRiverpodの紹介

2024/03/31に公開

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の作成

todoListProviderStateNotifierProviderとして定義し、Todoリストの状態を管理します。

このProviderをUI側で監視して、TodoのCRUD操作を行うメソッドを呼び出したり、Todoリストを取得します。

final todoListProvider = StateNotifierProvider<TodoListNotifier, List<TodoItem>>((ref) {
  final localDataRepositoryImpl = ref.read(localDataRepositoryProvider);
  return TodoListNotifier(localDataRepositoryImpl: localDataRepositoryImpl);
});

4. TodoListNotifierの実装

TodoListNotifierStateNotifier<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