AsyncValueを使用したAPI通信の実装【Flutter】

riverpodのstableのバージョン1.0.0が2021年11月5日に発表されました。(本記事公開日(2021/12/6)時点では1.0.2がstableの最新バージョン)

これによりFlutterを使用したアプリ開発の設計において、riverpodが選択される機会も増えてきたのではないかと思います。

今回は、そんなriverpodパッケージに含まれるAsyncValueクラスを利用した、API通信の実装方法について記していきます。

## AsyncValueとは
[AsyncValue](https://pub.dev/documentation/riverpod/latest/riverpod/AsyncValue-class.html)は、非同期的に更新されるデータを安全に取り扱うためにRiverPodに内包されている、ユーティリティクラスです。

`AsyncValue<T>`で定義される総称型である為、データクラスやEntityなどのモデルをパラメータで渡す事によって、`loading`や`error`といったデータの状態と、取得完了後のデータをまとめてハンドリングすることが特徴です。

[freezed](https://pub.dev/packages/freezed)などを使ってユニオン型に状態を定義して扱っていた場合などは、AsyncValueにより必要なコード量を減らすことができます:sparkle:

## 実装
### 前提
[QiitaのAPI](https://qiita.com/api/v2/items?page=1&per_page=10)から記事リストを取得して表示するサンプルです。

使用package

- [flutter_riverpod](https://pub.dev/packages/flutter_riverpod)(ver 1.0.2)
  - riverpodをFlutterプロジェクトで使用する為のpackageです。
- [dio](https://pub.dev/packages/dio)(ver 4.0.4)
  - 本記事ではHTTP Clientにdioを使用しています。(ここはretrofitでもchopperでもhttpでも何でも大丈夫です)
- [freezed](https://pub.dev/packages/freezed)(ver 1.0.2)
- [freezed_annotation](https://pub.dev/packages/freezed_annotation)(ver 1.0.0)
  - サンプルで表示する記事データのクラス(`Article`)をimmutableに扱うために使用しています。
- [json_serializable](https://pub.dev/packages/json_serializable)(ver 6.1.1)
  - APIからのレスポンスを、json(`Map<String, dynamic>`型)から`Article`型にパースするために使用しています。
- [build_runner](https://pub.dev/packages/build_runner)(ver 2.1.5)
    - .freezed及び.gファイルの自動生成を実行するのに必要です。

### 記事リスト取得
`FutureProvider`を使用し、APIからのレスポンスを`Article`型のListとして提供しています。

```dart
final articleListProvider = FutureProvider<List<Article>>((ref) async {
  var dio = Dio();
  final response = await dio
      .get<List<dynamic>>('https://qiita.com/api/v2/items?page=1&per_page=20');
  return response.data!.map((data) => Article.fromJson(data)).toList();
});
```

この`articleListProvider`をViewでwatchすることで、AsyncValueを扱うことができます。

### 記事リスト表示
前節で作成した`articleListProvider`をwatchし、その状態を`.when()`でハンドリングしています。
今回の実装では、

- 記事リストの取得中はインジケータを表示
- 記事の取得に成功した場合は件数に応じて記事のタイトルとLGTM、コメントの数をListViewで表示
- 記事の取得に失敗した場合は「エラーが発生しました」と表示

の3状態が存在します。

```dart
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final AsyncValue<List<Article>> articleList =
        ref.watch(articleListProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Qiita Client Demo'),
      ),
      body: Center(
        child: articleList.when(
          data: (articles) => ListView.builder(
            itemCount: articles.length,
            itemBuilder: (context, index) => Column(
              children: [
                ListTile(
                  title: Text(articles[index].title),
                  subtitle: Text(
                      'LGTM: ${articles[index].likes_count} コメント: ${articles[index].comments_count}'),
                  contentPadding: const EdgeInsets.all(16),
                  tileColor: Colors.lightGreen,
                ),
                const SizedBox(height: 8),
              ],
            ),
          ),
          error: (error, stackTrace) =>
              Text('エラーが発生しました。\n ${error.toString()}'),
          loading: () => const CircularProgressIndicator(),
        ),
      ),
    );
  }
```

### 実行結果
状態に応じてViewの表示が変わっていることが確認できました:+1:

| 記事リスト取得中 | 取得完了 | 取得失敗 |
|:-----------:|:------------:|:------------:|
|![Simulator Screen Shot - iPhone 13 Pro - 2021-12-06 at 19.18.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198029/95f1de59-e91a-6a67-71ff-20b2e14b436b.png)|![Simulator Screen Shot - iPhone 13 Pro - 2021-12-06 at 19.18.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198029/9c89b338-7695-cefb-944a-a253b4d95c29.png)|![Simulator Screen Shot - iPhone 13 Pro - 2021-12-06 at 19.20.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/198029/096204de-0864-af35-69d3-11b1d200a25e.png)|

以上、Volare Advent Calendar 2021の6日目記事でした！

https://qiita.com/advent-calendar/2021/volare

## 参考
- [riverpod.dev](https://riverpod.dev/)
- [RiverPod Package - AsyncValue](https://pub.dev/documentation/riverpod/latest/riverpod/AsyncValue-class.html)
- [flutter_riverpod](https://pub.dev/packages/flutter_riverpod)
- [dio](https://pub.dev/packages/dio)