Chapter 06

AsyncNotifierの使い方

JboyHashimoto
JboyHashimoto
2023.03.23に更新

https://docs-v2.riverpod.dev/docs/providers/notifier_provider

NotifierProvider は、Notifier をリッスンして公開するために使用されるプロバイダです。AsyncNotifier は、非同期に初期化することができる Notifier です。AsyncNotifierProvider は、AsyncNotifier をリッスンして公開するために使用されるプロバイダです。
(Async)NotifierProviderと(Async)Notifierは、ユーザーの操作に反応して変化する状態を管理するためのRiverpodの推奨ソリューションである。

これは、通常、次のような用途に使用されます。

カスタムイベントに反応した後、時間の経過とともに変化する可能性のある状態を公開する。
状態を変更するためのロジック(別名「ビジネスロジック」)を一箇所に集中させ、長期的な保守性を向上させる。
使用例として、Todo-Listを実装するためにNotifierProviderを使用することができます。そうすることで、addTodoのようなメソッドを公開し、ユーザーインタラクションでUIがTodoのリストを変更できるようにします。


出たばかりで、あまり情報がなかったので、CODE ANDDREAを参考にして、匿名ログインの機能を実装するプログラクムを作成しました。
登録せずに利用のボタンを押すと、ローディング処理がボタンの位置に表示され、Hello Worldしてくれるページへ移動します。このときに、匿名ログインをしたユーザーの情報がFirebaseAuthenticationに登録されます。
コールバック関数の知識も必要な場面があるので、解説します。

https://codewithandrea.com/articles/flutter-riverpod-async-notifier/#how-does-asyncnotifier-work?

AsyncNotifierはどのように機能するのですか?
Riverpodのドキュメントでは、AsyncNotifierを非同期に初期化されるNotifierの実装として定義しています。

そして、それを使ってauthAsyncNotifierControllerクラスを変換する方法を紹介する。

callbackとは?

https://developer.mozilla.org/ja/docs/Glossary/Callback_function
コールバック関数とは、引数として他の関数に渡され、外側の関数の中で呼び出されて、何らかのルーチンやアクションを完了させる関数のことです。

簡単な例を以下に示します。

function greeting(name) {
  alert(`Hello, ${name}`);
}

function processUserInput(callback) {
  const name = prompt("Please enter your name.");
  callback(name);
}

processUserInput(greeting);

Dartだとこんな感じですね。

void main() {
  greet('Hi');
  callBack(greet);
}

void greet(String s) {
  print(s); 
}

void callBack(Function function) {
  print(function);
}

匿名認証について

https://firebase.google.com/docs/auth/flutter/anonymous-auth?hl=ja
使うのは簡単でFirebaseのコンソールから設定をして、コードを書くだけでできます。

AsyncNotifierを定義する
状態を持っている変数とメソッドを扱うことができるAsyncNotifierを定義します。今回だと匿名でログインをすると、ボタンにローディングが表示される機能を実装しました。

// 1. add the necessary imports
import 'dart:async';
import 'dart:developer';
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/about_asyn_notifier/auth_page.dart';
import 'package:state_tutorial/about_asyn_notifier/hello_page.dart';

// authAsyncNotifierControllerを外部ファイルで呼び出すプロバイダー.
final authAsyncNotifierController = AsyncNotifierProvider(AuthController.new);

// FirebaseAuthをインスタンス化したプロバイダー.
final authRepositoryProvider =
    Provider<FirebaseAuth>((ref) => FirebaseAuth.instance);

class AuthController extends AsyncNotifier<void> {
  
  FutureOr<void> build() {
    // 値を返す(返り値が void ならば何もしない)。
  }
  // 匿名認証でログインするメソッド
  Future<void> signInAnonymously(BuildContext context) async {
    final authRepository = ref.read(authRepositoryProvider);
    state = const AsyncLoading();
    state = await AsyncValue.guard(authRepository.signInAnonymously);
    log(state.toString()); // 匿名ユーザーが作成されたのを表示するログ.
    Navigator.of(context).pushAndRemoveUntil(
        MaterialPageRoute(builder: (context) => const HelloPage()),
        (route) => false);
  }

  // ログアウトするメソッド.
  Future<void> signOutAnonymously(BuildContext context) async {
    final authRepository = ref.read(authRepositoryProvider);
    state = const AsyncValue.loading();
    state = await AsyncValue.guard(() => authRepository.signOut());
    Navigator.of(context).pushAndRemoveUntil(
        MaterialPageRoute(builder: (context) => const AsyncNotifierAuth()),
        (route) => false);
  }
}

コンポーネントに引数を渡す
プロバイダーをボタンコンポーネントのref.listenの第一引数に渡します。これは、ただのコールバック関数だそうで、やっていることは、AsyncNotifierを引数として渡してボタンが押されると、ローディング処理が実行される仕組みになっています。
もし、エラーが発生したらスナックバーを表示します。

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/auth/auth_state.dart';

// auth button that can be used for signing in or signing out
class AuthButton extends ConsumerWidget {
  const AuthButton({super.key, required this.text, required this.onPressed});
  final String text;// ボタンのタイトルを引数で渡す.
  final VoidCallback onPressed;// VoidCallback型を指定すると、ref.readを書くことができる.

  
  Widget build(BuildContext context, WidgetRef ref) {
    // ref.listenを使ってプロバイダーをコールバック関数の引数に渡す.
    ref.listen<AsyncValue<void>>(
      authAsyncNotifierController,
      (_, state) {
        if (state.hasError) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(state.asError.toString())),
          );
        }
      },
    );
    final state = ref.watch(authAsyncNotifierController);
    return SizedBox(
      width: 200,
      height: 60,
      child: ElevatedButton(
        onPressed: state.isLoading ? null : onPressed,
        child: state.isLoading
            ? const CircularProgressIndicator()
            : Text(text,
                style: Theme.of(context)
                    .textTheme
                    .headline6!
                    .copyWith(color: Colors.white)),
      ),
    );
  }
}

ログインページを作る

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/about_asyn_notifier/auth_controller.dart';
import 'package:state_tutorial/auth/auth_button.dart';

class AsyncNotifierAuth extends ConsumerWidget {
  const AsyncNotifierAuth({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('AsyncNotifier'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            AuthButton(
                text: '登録せずに利用',
                onPressed: () => ref
                    .read(authAsyncNotifierController.notifier)
                    .signInAnonymously(context)),
          ],
        ),
      ),
    );
  }
}

ログイン後のページ

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/auth/auth_state.dart';

class HelloPage extends ConsumerWidget {
  const HelloPage({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    final authController = ref.read(authAsyncNotifierController.notifier);

    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
              onPressed: () async {
                authController.signOutAnonymously(context);
              },
              icon: const Icon(Icons.logout))
        ],
        title: const Text('Auth'),
      ),
      body: Center(child: Text('Hello World')),
    );
  }
}

アプリを実行するコード

main.dart
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:state_tutorial/about_asyn_notifier/auth_page.dart';

import 'firebase_options.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  runApp(ProviderScope(child: MyApp()));
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  // This widget is the root of your application.
  
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const AsyncNotifierAuth(),
    );
  }
}

実行結果