Qiitaクライアント開発でRiverpod AsyncValue使ってみた


6/21追記 新しいFlutterやライブラリのバージョンに対応した記事を書きました。
【Flutter 2.2.1】hooks_riverpodやretrofit、freezedを使ってQiita APIから記事を取得

今回やりたいこと

最近、個人でコードを書くときはRiverpodを使っていますが、何となくまだまだRiverpodについてしっかり使えてないと感じることが多いので、公式ドキュメントや関連記事などを見ていたところ、AsyncValueというものが便利そうだったのでQiitaクライアントサンプルで使ってみることにしました。

AsyncValueとは

公式ドキュメント
https://pub.dev/documentation/riverpod/latest/all/AsyncValue-class.html

公式ドキュメントの説明にある通り、非同期データを安全に扱えるものです。
またサンプルにある通り、loading, errorなどの状態の処理を忘れずにハンドリングすることが出来るようになります。

return user.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Oops, something unexpected happened'),
      data: (value) => Text('Hello ${user.name}'),
    );

AsyncValueを使ってみる

前提として、過去にRiverpod+StateNotifierでQiitaの記事を取得する記事を書いています。

今回はそのときに使用したコードをベースにAsyncValueを使います。

今回主に変更されるファイルはarticle_state_notifier.dartとarticle_screen.dartになります。

article_state_notifier.dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qiita_sample/data/entities/qiita_info.dart';
import 'package:qiita_sample/screens/article/article_repository.dart';
import 'package:state_notifier/state_notifier.dart';

class ArticleStateNotifier extends StateNotifier<AsyncValue<List<QiitaInfo>>> {
  ArticleStateNotifier(this.repository) : super(const AsyncValue.loading()) {
    _getFlutterArticles();
  }

  final ArticleRepository repository;

  Future<void> _getFlutterArticles() async {
    try {
      final articles = await repository.getFlutterArticles();
      state = AsyncValue.data(articles);
    } on Exception catch (e) {
      state = AsyncValue.error(e);
    }
  }
}
article_screen.dart

// (略)

class _List extends StatelessWidget {
  final articleStateNotifier = StateNotifierProvider(
    (_) => ArticleStateNotifier(
      ArticleRepository(),
    ),
  );

  @override
  Widget build(BuildContext context) {
    return Consumer(
      builder: (context, watch, child) {
        final AsyncValue<List<QiitaInfo>> articles = watch(articleStateNotifier.state);
        // AsyncValueでdata,loading,errorのハンドリングが自動で生える
        return articles.when(
            data: (data) => ListView.builder(
                itemBuilder: (context, int position) => ArticleItem(
                  qiitaInfo: data[position],
                  onPressed: (qiitaInfo) => _openArticleWebPage(
                    context,
                    qiitaInfo,
                  ),
                ),
            ),
            loading: () => Center(
              child: CircularProgressIndicator(),
            ),
            error: (_, __) => Center(
              child: Text('データの取得に失敗しました。'),
            ),
        );
      },
    );
  }

// (略)

今回の主な変更は以上になります。

ちなみに、ArticleStateNotifierで、ArticleStateを使用しなくなったので、

article_state.dart
import 'package:flutter/foundation.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:qiita_sample/data/entities/qiita_info.dart';

part 'article_state.freezed.dart';
part 'article_state.g.dart';

@freezed
abstract class ArticleState with _$ArticleState {
  const factory ArticleState({
    @Default([]) List<QiitaInfo> articles,
    @Default(false) showLoadingIndicator,
  }) = _ArticleState;

  factory ArticleState.fromJson(Map<String, dynamic> json) =>
      _$ArticleStateFromJson(json);
}

上記のarticle_state.dartファイルを削除します。取得した記事を持つarticlesや、LoadingIndicatorの状態の管理がAsyncValueで出来るようになりました。

article_state.dartを削除した後は、build runnerコマンドを実行します。

flutter packages pub run build_runner build --delete-conflicting-outputs

アプリを実行すると、問題なく動作しています。

余談

今までこのQiitaクライアントのサンプルでLoadingIndicatorを表示させていなかったので気付きませんでしたが、これまでのサンプル通りだと、LoadingIndicatorが表示されて記事を取得するとき、一瞬LoadingIndicatorが止まってしまうような動きになってしまいました。

原因はqiita_api_client.dart内のログ出力でした。特にQiita記事のように情報量の多いJSON情報を出力することで、アプリが一瞬止まるようになっていたようです。。毎回JSON情報を確認したいこともないので、削除しました。

qiita_api_client.dart
@RestApi(baseUrl: "https://qiita.com/api/v2")
abstract class QiitaApiClient {
  factory QiitaApiClient(Dio dio, {String baseUrl}) = _QiitaApiClient;

  static QiitaApiClient create() {
    final dio = Dio();
    // 以下のログ出力を削除する
    // dio.interceptors.add(PrettyDioLogger());
    return QiitaApiClient(dio);
  }

  @GET("/tags/flutter/items?per_page=50")
  Future<List<QiitaInfo>> getFlutterArticles();
}

まとめ

AsyncValueを使うことで今回のサンプルのように、APIから情報を取得して、ローディング中はインジケーターを表示させたり、エラーが発生した場合のハンドリングを行う場合に、ArticleStateが不要になり、かなりコード量を少なく書けるようになりました。

まだまだRiverpodに慣れていないので、引き続き色々使ってみます。

今回のサンプルコード