Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Riverpod & Riverpod Generatorを利用して状態管理部分の処理を書き換...

Riverpod & Riverpod Generatorを利用して状態管理部分の処理を書き換えてみる簡単な事例紹介

3/26に開催されたMobile勉強会 ウォンテッドリー × チームラボ × Sansan #19での登壇資料になります。

Flutter向けの状態管理ライブラリ「Riverpod」において、1.x系から2.x系への移行ポイントと、コード生成ツール(Riverpod Generator)を活用した効率的な実装方法を紹介しています。StateNotifierやStateProviderが非推奨となり、代わりにNotifierやAsyncNotifierを使った書き換え例、Firestoreを用いたCRUD処理との連携例、さらにfreezedやRiverpod Generatorを活用してボイラープレートを削減する事例などを示しており、Riverpod 2.xへの移行手順や実践的なコード例を中心に解説しています。

⭐️詳細解説記事はこちらになります。
https://zenn.dev/fumiyasac/articles/66023a2b2b0691

Fumiya Sakai

March 25, 2025
Tweet

More Decks by Fumiya Sakai

Other Decks in Technology

Transcript

  1. 自己紹介 ・Fumiya Sakai ・Mobile Application Engineer アカウント: ・Twitter: https://twitter.com/fumiyasac ・Facebook:

    https://www.facebook.com/fumiya.sakai.37 ・Github: https://github.com/fumiyasac ・Qiita: https://qiita.com/fumiyasac@github 発表者: ・Born on September 21, 1984 これまでの歩み: Web Designer 2008 ~ 2010 Web Engineer 2012 ~ 2016 App Engineer 2017 ~ Now iOS / Android / sometimes Flutter
  2. 新しいバージョンのRiverpodにおける変更に関して Riverpod1.x系→2.x系へ移行する際のアプローチに関する内容になります Flutterは個人的に少し触っている程度ではありますが、今回気になったので調査をした次第です。 1. メジャーバージョンアップによる大きな変更点: これまで良く利用されていたStateNotifierやStateProviderは、Riverpod2.x系からは非推奨となっており、書き換える必要があ ります。より直感的かつシンプルなAsyncNotifierやNotifierの使用が推奨されています。 2. Riverpod Generatorの併用:

    Riverpod Generatorを併用することで、ボイラープレートコードを大幅に削減できるようになります。これにより、開発効率の 向上やコード品質の安定化も見込む事ができます。 3. 簡単コードを交えた事例紹介: シンプルな例を通じてRiverpodの基本的な概念と実装方法を段階的に学びつつ、コード量削減・保守性向上・開発効率改善を具 体的なコード例とともに解説する内容です。
  3. 実際にStateNotifierを利用して記述している箇所 Firestore経由でデータ一覧を表示する部分だけでなく入力時も利用している 非推奨となったStateNotifierをまずは手動で置き換えてるアプローチについて考えてみる事にします。 利用箇所1 Firestoreを利用したデータ一覧表示処理 利用箇所2 新規でデータを入力する際の処理 ToDoリストの一覧表示を 実施している箇所につい ては解説動画内では、

    StateNotifierを利用 ラジオボタンでのカテゴ リー選択や日付や時間設 定するための処理でも、 StateNotifierを利用 ※元々の画面構成をより 良い形にするために画面 リファクタリングとして Stream & StreamBuilder を用いた表示方針へ変更 ※入力フォーム内におけ る選択内容の状態管理を する箇所についても手動 で書き換えを実施
  4. StateProviderを利用した書き方をしている場合 サンプル内では元々この様な書き方をしていました(自動生成を利用しない) final radioProvider = StateProvider<int>((ref) { return 0; });

    final dateProvider = StateProvider<String>((ref) { return "dd/mm/yy"; }); final timeProvider = StateProvider<String>((ref) { return "hh : mm"; }); // 👉 ラジオボタン選択処理用のProvider利用箇所での処理 // ref.read(radioProvider.notifier).update((state) => 1); // 👉 日付選択処理用のProvider利用箇所での処理 // ref.read(dateProvider.notifier).update((state) => format.format(getDate)); // 👉 時間選択処理用のProvider利用箇所での処理 // ref.read(timeProvider.notifier).update((state) => getTime.format(context)); Widget内で利用 View (Widget) Provider Model Firestoreからのデータ 取得及び削除処理、完了 状態へのステータス変更 を実施するための処理 ①ラジオボタン・日付・ 時間の入力に関わる部分 をハンドリングする処理 ②Firestoreを利用処理 Repository
  5. 入力で利用していたStateProviderを置換する NotifierProviderを利用した実装に置き換えていく事が必要になります。 final radioProvider = NotifierProvider<RadioNotifier, int>(RadioNotifier.new); class RadioNotifier extends

    Notifier<int> { @override int build() => 0; void update(int radioValue) { state = radioValue; } } final dateProvider = NotifierProvider<DateNotifier, String>(DateNotifier.new); class DateNotifier extends Notifier<String> { @override String build() => "dd/mm/yy"; void update(String dateValue) { state = dateValue; } } final timeProvider = NotifierProvider<TimeNotifier, String>(TimeNotifier.new); class TimeNotifier extends Notifier<String> { @override String build() => "hh : mm"; void update(String timeValue) { state = timeValue; } } // 👉 ラジオボタン選択処理用のProvider利用箇所での処理 // ref.read(radioProvider.notifier).update(1); // 👉 日付選択処理用のProvider利用箇所での処理 // ref.read(dateProvider.notifier).update(format.format(getDate)); // 👉 時間選択処理用のProvider利用箇所での処理 // ref.read(timeProvider.notifier).update("hh : mm"); Widget内で利用 既存のStateNotifierを置換する際はこの様な流れになります。 1. Notifier<T>クラスを継承して状態変化用のクラスを作成する 2. NotifierProvider<NotifireClass, Type>のインスタンスを作成する state値の更新については、Notifier<T>継承クラス内に更新用のメソッド を準備して、これを利用して任意の利用Widgetから利用します。
  6. Firestore関連処理をNotifierProviderに置き換える シンプルなCRUD処理の形であれば下記の様な感じで置き換え可能 class TodoRepository { final todoCollection = FirebaseFirestore.instance.collection('todoApp'); //

    👉 FirestoreからのDocument取得処理についてはStreamを利用して取得する形に変更しています。 // ※ View側でToDoデータ一覧を取得して表示する処理についても`StreamBuilder`を使用しています。 Stream<List<TodoModel>> fetchTasks() { return todoCollection.snapshots() .map((event) => event.docs.map((snapshot) => TodoModel.fromSnapshot(snapshot)).toList() ); } void addNewTask(TodoModel model) { todoCollection.add(model.toMap()); } void updateTask(String? docID, bool? valueUpdate) { todoCollection.doc(docID).update({ 'isDone': valueUpdate, }); } void deleteTask(String? docID) { todoCollection.doc(docID).delete(); } } final todoProvider = NotifierProvider<TodoRepositoryNotifier, TodoRepository>(TodoRepositoryNotifier.new); class TodoRepositoryNotifier extends Notifier<TodoRepository> { @override TodoRepository build() => TodoRepository(); } Provider定義 Notifier<T>では、T:TodoRepositoryを設定する CRUD処理を定義したRepositoryクラスを@overrideに追加する
  7. Riverpod Generateor & freezedを利用した処理例(1) @freezedによるコード自動生成ベースの実装 @freezed class Comment with _$Comment

    { // プロパティを定義する const factory Comment({ required String id, required String bookId, required String userId, required String content, required DateTime createdAt, }) = _Comment; // JSON形式で受け取るためのコードを定義する factory Comment.fromJson(Map<String, dynamic> json) => _$CommentFromJson(json); // Firestoreからデータを受け取ってJSON形式に変換する factory Comment.fromFirestore(DocumentSnapshot doc) { final data = doc.data() as Map<String, dynamic>; return Comment.fromJson({ ...data, 'id': doc.id, 'createdAt': (data['createdAt'] as Timestamp).toDate().toIso8601String(), }); } } @freezed class Book with _$Book { // プロパティを定義する const factory Book({ required String id, required String title, required String author, required String summary, required String isbn, required String userId, required DateTime createdAt, }) = _Book; // JSON形式で受け取るためのコードを定義する factory Book.fromJson(Map<String, dynamic> json) => _$BookFromJson(json); // Firestoreからデータを受け取ってJSON形式に変換する factory Book.fromFirestore(DocumentSnapshot doc) { final data = doc.data() as Map<String, dynamic>; return Book.fromJson({ ...data, 'id': doc.id, 'createdAt': (data['createdAt'] as Timestamp).toDate().toIso8601String(), }); } }
  8. Riverpod Generateor & freezedを利用した処理例(2) Firestoreからのデータ取得及び削除処理・新規追加処理を実施 @riverpod BookRepository bookRepository(Ref ref) {

    return BookRepository(FirebaseFirestore.instance); } class BookRepository { final FirebaseFirestore _firestore; BookRepository(this._firestore); Future<List<Book>> getBooks() async { final snapshot = await _firestore .collection('books') .orderBy('createdAt', descending: true) .get(); return snapshot.docs.map((doc) => Book.fromFirestore(doc)).toList(); } Future<void> addBook(String title, String author, String summary, String isbn, String userId) async { await _firestore.collection('books').add({ 'title': title, 'author': author, 'summary': summary, 'isbn': isbn, 'userId': userId, 'createdAt': FieldValue.serverTimestamp(), }); } Future<void> deleteBook(String id) async { await _firestore.collection('books').doc(id).delete(); } }
  9. Riverpod Generateor & freezedを利用した処理例(3) Firestoreからのデータ取得及び削除処理・新規追加処理を実施 @riverpod CommentRepository commentRepository(Ref ref) {

    return CommentRepository(FirebaseFirestore.instance); } class CommentRepository { final FirebaseFirestore _firestore; CommentRepository(this._firestore); Future<List<Comment>> getComments(String bookId) async { final snapshot = await _firestore .collection('comments') .where('bookId', isEqualTo: bookId) .orderBy('createdAt', descending: true) .get(); return snapshot.docs.map((doc) => Comment.fromFirestore(doc)).toList(); } Future<void> addComment(String bookId, String userId, String content) async { await _firestore .collection('comments').add({ 'bookId': bookId, 'userId': userId, 'content': content, 'createdAt': FieldValue.serverTimestamp(), }); } Future<void> deleteComment(String bookId) async { final snapshot = await _firestore .collection('comments') .where('bookId', isEqualTo: bookId) .get(); for (var doc in snapshot.docs) { doc.reference.delete(); } } }
  10. Riverpod Generateor & freezedを利用した処理例(4) Repositoryクラスの処理をWidgetから実行できる様にする仲介役 @riverpod class BookViewModel extends _$BookViewModel

    { @override Future<List<Book>> build() async { return ref.watch(bookRepositoryProvider).getBooks(); } Future<void> addBook(String title, String author, String summary, String isbn, String userId) async { await ref.read(bookRepositoryProvider).addBook(title, author, summary, isbn, userId); ref.invalidateSelf(); } Future<void> deleteBook(String id) async { await ref.read(bookRepositoryProvider).deleteBook(id); await ref.read(commentRepositoryProvider).deleteComment(id); ref.invalidateSelf(); } } @riverpod class CommentViewModel extends _$CommentViewModel { @override Future<List<Comment>> build(String bookId) async { return ref.watch(commentRepositoryProvider).getComments(bookId); } Future<void> addComment(String bookId, String userId, String content) async { await ref.read(commentRepositoryProvider).addComment(bookId, userId, content); ref.invalidateSelf(); } }