【Flutter】AsyncNotifierProviderを用いたUI更新

こんにちは、株式会社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(状態ごとの処理)終了
          ],
        ),
      ),
    );
  }
}

todo-async-notifier.gif

まとめ

以上がAsyncNotifierProviderを使用してUIを更新する方法になります。これにより、非同期の通知を簡単に実装することができます。ぜひ利用してみてください。

採用情報はこちら
目次