🏔️

AsyncValueは要らない子な気がしている

2022/07/31に公開

RiverpodのAsyncValueというのがあります。
非同期の値を取ってくる時に、data loading error の3つのステータスに自動的に割り振ってくれ、各々の状態に適したUIを返すことが出来るもの、です。

Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<List<String>> items =
        ref.watch(itemsProvider);

    return Scaffold(     
      body: Center(
        child: items.when(
          data: (e) => ListView.builder(
            itemCount: items.length,
            itemBuilder: (context, index) => Text(e)
          ),
          error: (error, stackTrace) =>
              Text('${error.toString()}'),
          loading: () => const CircularProgressIndicator(),
        ),
      ),
    );
  }

自分もこのAsyncValueを使っていて、以下の簡易的なラッパーウイジェットを使っていた。


class AsyncValueWidget<T> extends StatelessWidget {
  const AsyncValueWidget({Key? key, required this.value, required this.data}) : super(key: key);
  final AsyncValue<T> value;
  final Widget Function(T) data;

  
  Widget build(BuildContext context) {
    return value.when(
        data: data,
        loading: () => const Center(
                child: CircularProgressIndicator(),
              ),
        error: (error, stacktrace) {
          return Center(
            child: Text(
              error.toString(),
            ),
          );
        });
  }
}

最近は要らない子だと感じてる。

  • 非同期処理のローディングはグローバルなインジケーターで代用できる。StateNotifierProviderでtrue/falseを更新かけて、trueならStackで上に積んだローダーを表示することができる。
  • AsyncValueで取ってきたデータを更新するケースに、loadingerrorのステータスは要らない
  • success,errorの状態管理は、原則的にFutureが必要なビジネスロジックの実行時に常に必要になる。

wasabeefさんのレポジトリのコードを参考にさせてもらった。これで充分。

class Result<T> with _$Result<T> {
  const Result._();
  const factory Result.success({required T data}) = Success<T>;
  const factory Result.failure({required AppError error}) = Failure<T>;

  /// 同期のビジネスロジック用
  static Result<T> guard<T>(T Function() body) {
    try {
      return Result.success(data: body());
    } on Exception catch (e) {
      return Result.failure(error: AppError.getAppError(e));
    }
  }

  /// ネットワーク通信など、非同期で呼び出すロジック用
  static Future<Result<T>> guardFuture<T>(Future<T> Function() future) async {
    try {
      return Result.success(data: await future());
    } on Exception catch (e) {
      return Result.failure(error: AppError.getAppError(e));
    }
  }
}

グローバルなインジケーターについては、以下を参考にさせてもらってもらった。
https://future-architect.github.io/articles/20220329a/

自分のプロフィール編集みたいな画面がある場合、AsyncValueはうっとおしい。AsyncValueでデータを取ってきてしまうと、ステータスを一度loadingにしてから更新データを戻す事が必要だった。AsyncValueは非同期に取ってきた値の加工が無い前提で使うものかもしれない。

気をつける点が1個あって、StateNotifierProviderを初期化する時にインジケーターを出したい場合は、上記のインジケーターのwrap関数で、stateを初期化しないとダメ。自分の画面のリビルドをawaitしたあとで、インジケーターのStateNotifierをリビルドする必要がある。

final sampleStateController =
    StateNotifierProvider.autoDispose<SampleController, SampleData>(
        (ref) => SampleController(ref));

class SampleController extends StateNotifier<SampleData> {
  final Ref ref;

  SampleController(this.ref) : super(const SampleData()) {
    fetchSample();
  }

 Future<void> fetchSample() async {
    ref.read(loadingServiceProvider.notifier).wrap(getData());
  }

  Future<void> getData() async {
    //この場合は
    state = await ref.read(sampleRepository).getData();
  }
}

今動いているのを差し替えるほどの情熱もメリットもないけど、今後作るアプリはAsyncValueを全廃する方向で考えています。

追記 2023.02.25

Riverpod2系のAsyncNotiferで非同期の値の取り回しが書きやすくなったので、そっちで今後書いていこうと思っています。

Discussion