こんにちは、株式会社Pentagonでアプリ開発をしている中原です。
RiverpodのAsyncNotifierProviderを使用してUIを更新する方法について学習したため、記事にまとめました。
AsyncNotifierProviderはRiverpod2.0からProviderに追加されました。以前からあるStateNotiferProviderの使用は非推奨となり、AsyncNotifierProvider、NotifierProviderの使用が推奨となリました。今回はAsyncNotifierProviderの基本的な使い方についてご紹介します。
【こんな人に読んで欲しい】
- Flutterアプリ開発エンジニア
- Riverpod 2.0以降、情報をアップデートしていない人
【この記事を読むメリット】
- 非同期で状態を監視して、UIを更新することができる。
- AsyncNotifierProviderの基本的な使い方を理解できる。
【結論】
AsyncNotifierProviderは非同期で状態を更新を行いたい場合に使用します。
非同期で実行されるため複数の処理を並列で実行でき、他の操作を待たずにデータが更新を行うことができます。つまり、データを追加、更新、または削除の処理を実行しつつ、最新の状態に更新することができます。
それでは、AsyncNotifierProviderの使い方を見ていきましょう。
この記事で触れないこと
この記事では、Firebaseを使用したTodoアプリの実装においてAsyncNotifierProviderを使用しています。以下の実装や説明については省略しています。
- Flutterの開発環境構
- Firebaseの環境構築
- Todoモデルの実装
- Freezedの説明
開発環境
- Flutter 3.7.12
- Dart 2.19.6
- vscode
パッケージ
AsyncNotifierProviderはパッケージにflutter_riverpodを追加することで使用することができます。
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
flutter_riverpod: ^2.3.6
freezed_annotation: ^2.2.0
json_annotation: ^4.8.1
firebase_core: ^2.11.0
cloud_firestore: ^4.6.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
build_runner: ^2.3.3
freezed: ^2.3.3
json_serializable: ^6.6.2
AsyncNotifierProviderとは
AsyncNotifierProviderは、非同期な状態とそれを操作するAsyncNotifierを提供するためのProviderです。
AsyncNotifierの状態が変更されるたびに通知され、状態の変更に応じて動的に値が更新されます。このプロバイダを使用することで状態の変更を監視し、UIを適切に更新することができます。通常、次の場合に使用されます。
- APIからのデータを操作する場合。
- イベント処理後に時間の経過とともに状態(state)が変化する場合
- ビジネスロジックを一箇所に集中させ、長期的な保守性を向上させたい場合
AsyncNotifierProviderの定義
AsyncNotifierProviderを使用するためには、AsyncNotifier
を継承したクラスを作成する必要があります。この継承されたクラスはAsyncValueの(data・loading・error)
状態を持ち操作することができます。
例外が発生した場合にAsyncError
を返し、guard()
メソッドを使用することでtry-catch
の記述を簡略化することもできます。
AsyncNotifierを使用する場合、初めにbuildメソッド
を作成する必要があり、AsyncNotifierProviderを呼び出した際に最初に呼ばれるメソッドです。buildメソッド内には初期値を返す処理を書く必要があります。またStateNotifierProviderと比較して、クラス内に Ref を記述する必要がないため、コードを少し削減することができます。
AsyncNotifierProviderは次の方法で定義します。
final プロバイダ名 = AsyncNotifierProvider<T>((ref) => Class名.new);
実際のコードを見ていきましょう。
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_async_notifier/todo.dart';
final fireStoreProvider =
Provider<FirebaseFirestore>((ref) => FirebaseFirestore.instance);
final collectionReferenceProvider = Provider<CollectionReference>(
(ref) => ref.read(fireStoreProvider).collection('todoList'));
// AsyncNotifierProviderを定義
final todoAsyncNotifierProvider =
AsyncNotifierProvider<TodoAsyncNotifier, List<Todo>>(TodoAsyncNotifier.new);
class TodoAsyncNotifier extends AsyncNotifier<List<Todo>> {
//refを渡さなくても読み取りが可能
// CollectionReferenceの取得
CollectionReference get collectionReference =>
ref.read(collectionReferenceProvider);
// build メソッドをオーバーライドして FutureOr を返す
@override
FutureOr<List<Todo>> build() async {
// 初期データの読み込み
return await fetchData();
}
// データの取得メソッド
Future<List<Todo>> fetchData() async {
final snapshots = await collectionReference.get();
return snapshots.docs.map((doc) => Todo.fromDocument(doc)).toList();
}
// データの追加メソッド
Future<void> add({required String title}) async {
// Todoの作成
final todo = Todo(title: title);
// stateをローディングにする(値がまだ利用可能でない状態)
state = const AsyncValue.loading();
// 例外の発生時は AsyncErrorを返す(try~catchを省くことができる)
state = await AsyncValue.guard(() async {
await collectionReference.add(todo.toJson());
return await fetchData();
});
}
// チェックボタンの更新メソッド
Future<void> toggle({required String id}) async {
final todo = state.value!.firstWhere((todo) => todo.id == id);
// stateをローディングにする(値がまだ利用可能でない状態)
state = const AsyncValue.loading();
// 例外の発生時は AsyncErrorを返す(try~catchを省くことができる)
state = await AsyncValue.guard(() async {
final updatedTodo = todo.copyWith(isCompleted: !todo.isCompleted);
await collectionReference.doc(id).update(updatedTodo.toJson());
return await fetchData();
});
}
// データの削除メソッド
Future<void> delete({required String id}) async {
// stateをローディングにする(値がまだ利用可能でない状態)
state = const AsyncValue.loading();
// 例外の発生時は AsyncErrorを返す(try~catchを省くことができる)
state = await AsyncValue.guard(() async {
await collectionReference.doc(id).delete();
return await fetchData();
});
}
}
上記のコードでは、AsyncNotifierProviderを定義し、AsyncNotifierのClassにはデータの取得、追加、更新、削除などの機能を定義しています。必要に応じてプロパティやメソッドを追加してください。
AsyncNotifierProviderを使用してUIを更新する
AsyncNotifierProviderは、ConsumerWidgetやHookWidgetなど、WidgetRefを持つウィジェットで使用します。
AsyncNotifierProviderは ref.watch(プロバイダー名);
のように呼び出され、AsyncValueの状態(state)を取得することができます。また、ref.watch(プロバイダー名.notifier);
とすることでAsyncNotifierのメソッドを呼び出すことができます。変数名に .when
を使用することで、data・loading・errorの状態(state)を監視する事ができます。
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:todo_async_notifier/firebase_options.dart';
import 'package:todo_async_notifier/todo.dart';
import 'package:todo_async_notifier/todo_async_notilier.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
// ProviderScopeでラップ
runApp(const ProviderScope(child: MyApp()));
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'sample',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: const MyHomePage(),
);
}
}
class MyHomePage extends ConsumerWidget {
const MyHomePage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final TextEditingController controller = TextEditingController();
// AsyncNotifierProviderを呼び出す。
// 非同期でAsyncValue<List<Todo>>の状態が取得できる。
final asyncValue = ref.watch(todoAsyncNotifierProvider);
// .notifierを使うと、AsyncNotifierのメソッドを呼び出すことができる。
final notifier = ref.watch(todoAsyncNotifierProvider.notifier);
return Scaffold(
appBar: AppBar(
title: const Text("sample"),
),
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 30),
child: Column(
children: [
// テキストフィールドとTodo作成ボタン
Row(
children: [
Expanded(
child: TextFormField(
controller: controller,
maxLength: 20,
),
),
IconButton(
onPressed: () async {
// Todoの追加
await notifier.add(title: controller.text);
},
icon: const Icon(Icons.play_arrow),
)
],
),
const Padding(
padding: EdgeInsets.symmetric(vertical: 30),
child: Text("TODO"),
),
// ここからasyncNotifierProvider(状態ごとの処理)
asyncValue.when(
// ローディング時の処理(値がまだ利用可能でない状態)
loading: () => const Center(child: CircularProgressIndicator()),
// エラー時の処理
error: (error, _) => Center(child: Text('Error: $error')),
// データの取得に成功した場合の処理
data: (data) => data.isNotEmpty
? Expanded(
child: ListView.builder(
itemCount: data.length,
itemBuilder: ((BuildContext context, int index) {
final todo = data[index];
return Row(
children: [
Expanded(
child: Row(
children: [
Checkbox(
value: todo.isCompleted,
onChanged: (_) async =>
// チェックボタンの更新
await notifier.toggle(id: todo.id!),
),
// isCompletedがtrueの場合は文字に斜線を入れ、色はグレーにする
Text(
todo.title,
style: TextStyle(
color: todo.isCompleted
? Colors.grey
: Colors.black,
decoration: todo.isCompleted
? TextDecoration.lineThrough
: TextDecoration.none,
),
),
],
),
),
IconButton(
// Todoの削除
onPressed: () async =>
await notifier.delete(id: todo.id!),
icon: const Icon(Icons.delete),
),
],
);
}),
),
)
: Container(),
),
// ここからasyncNotifierProvider(状態ごとの処理)終了
],
),
),
);
}
}
まとめ
以上がAsyncNotifierProviderを使用してUIを更新する方法になります。これにより、非同期の通知を簡単に実装することができます。ぜひ利用してみてください。