🪃

AsyncNotifierでref.listenを使う!

2023/09/11に公開

Overview

以前、AsyncNotifierとref.listenを使用して、エラー処理を作っていたのですがうまくいきませんでした。
今回試しに、コードを書いてたらうまくいったので記事にしようと思いました。

https://pub.dev/documentation/riverpod/latest/riverpod/AsyncError-class.html
エラー状態の AsyncValue を作成します。

StackTrace を持っていません。StackTrace.current.AsyncError を渡すことで、AsyncError を構築できます:

どのような時に使うかというと、AsyncNotifierで、非同期処理のエラーハンドリングをするときです。

summary

今回だと、Firestoreにデータをaddするときに、入力フォームの値がnullだったらref.listenを使って、スナックバーを出すエラーハンドリングをしています。

Firestoreにデータを保存するときに、stateの管理をして、nullだったら、AsyncErrorを使うロジックです。

import 'dart:async';

import 'package:architecture_app/src/features/app/data/post_provider.dart';
import 'package:architecture_app/src/features/app/domain/post/post.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

final postNotifierProvider =
    AsyncNotifierProvider<PostNotifier, void>(PostNotifier.new);

class PostNotifier extends AsyncNotifier<void> {
  
  FutureOr<void> build() {
    // 戻り値がなければ書かない。
  }

  Future<void> sendPost(Post post) async {
    final postRef = ref.read(fireStoreProvider);

    // 入力された値がnullだったら、ref.listenでエラーを表示する
    if (post.body.isEmpty) {
      state = const AsyncError<void>('投稿失敗: 本文が空です', StackTrace.empty);
    }
    // 入力された値がnullじゃなかったら、投稿する
    else {
      try {
        state = const AsyncLoading();
        await postRef.collection('post').add(post.toJson());
        state = const AsyncValue<void>.data(null); // Successfully added
      // ignore: avoid_catches_without_on_clauses
      } catch (e, stackTrace) {
        // ignore: noop_primitive_operations
        state = AsyncError<void>('投稿失敗: ${e.toString()}', stackTrace);
      }
    }
  }
}

ポイントは、AsyncErrorを使う時には、第1引数と第2引数が必要です。

// 入力された値がnullだったら、ref.listenでエラーを表示する
    if (post.body.isEmpty) {
      state = const AsyncError<void>('投稿失敗: 本文が空です', StackTrace.empty);
    }

View側はこんな感じになってます。null のデータが入ってきたら、スナックバーが出ます。

import 'package:architecture_app/src/features/app/application/post_notifier.dart';
import 'package:architecture_app/src/features/app/domain/post/post.dart';
import 'package:architecture_app/src/features/auth/application/auth_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

/// [ログイン後のページ]ここで、投稿と表示をする
class PostPage extends ConsumerWidget {
  const PostPage({super.key});

  
  Widget build(BuildContext context, WidgetRef ref) {
    final bodyController = TextEditingController();
    ref.listen<AsyncValue<void>>(
      postNotifierProvider,
      (prev, next) {
        if (next is AsyncError) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text(next.error.toString())),
          );
        }
      },
    );

    return Scaffold(
      appBar: AppBar(
        actions: [
          IconButton(
            icon: const Icon(Icons.lock),
            onPressed: () async {
              await ref.read(authNotifierProvider.notifier).signOut();
            },
          ),
        ],
        title: const Text('Post'),
      ),
      body: Center(
        child: Column(
          children: [
            SizedBox(
              width: 300,
              height: 50,
              child: TextFormField(
                controller: bodyController,
                decoration: const InputDecoration(
                  hintText: '本文',
                  border: OutlineInputBorder(),
                ),
              ),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
                onPressed: () async {
                  final post = Post(
                    body: bodyController.text,
                    createdAt: DateTime.now(),
                    updatedAt: DateTime.now(),
                  );
                  await ref.read(postNotifierProvider.notifier).sendPost(post);
                },
                child: const Text('投稿')),
          ],
        ),
      ),
    );
  }
}

入力した値がnullだったらこのようにスナックバーが出てきます!

thoughts

参考になりそうな情報がないので、手探りで今回エラーハンドリングする方法を作ってみました。こちらに作っている途中ですがソースコードがあるので、参考にされてみてください。

Githubのソースコード
https://github.com/sakurakotubaki/FutureFirstApp/tree/feature/auth

AsyncErrorとはなんだったのかというと、機能が集約されたクラスのようです。

/// {@macro asyncvalue.error_ctor}
class AsyncError<T> extends AsyncValue<T> {
  /// {@macro asyncvalue.error_ctor}
  const AsyncError(Object error, StackTrace stackTrace)
      : this._(
          error,
          stackTrace: stackTrace,
          isLoading: false,
          hasValue: false,
          value: null,
        );

  const AsyncError._(
    this.error, {
    required this.stackTrace,
    required T? value,
    required this.hasValue,
    required this.isLoading,
  })  : _value = value,
        super._();

  
  final bool isLoading;

  
  final bool hasValue;

  final T? _value;

  
  T? get value {
    if (!hasValue) {
      throwErrorWithCombinedStackTrace(error, stackTrace);
    }
    return _value;
  }

  
  final Object error;

  
  final StackTrace stackTrace;

  
  R map<R>({
    required R Function(AsyncData<T> data) data,
    required R Function(AsyncError<T> error) error,
    required R Function(AsyncLoading<T> loading) loading,
  }) {
    return error(this);
  }

  
  AsyncError<T> copyWithPrevious(
    AsyncValue<T> previous, {
    bool isRefresh = true,
  }) {
    return AsyncError._(
      error,
      stackTrace: stackTrace,
      isLoading: isLoading,
      value: previous.valueOrNull,
      hasValue: previous.hasValue,
    );
  }
}

英語のコメントを翻訳するとこんなことが書かれていました。

私の解釈ですけど、AsyncValue型のNotifierに、whenメソッドを追加してエラー処理ができるようにしたものなのかなと思いました。
間違ってたらすいません🙇‍♂️

Discussion