【Flutter 2.2.1】hooks_riverpodやretrofit、freezedを使ってQiita APIから記事を取得

## はじめに

今回のサンプルの画面は以下になります。

<img width="250" alt="今回のサンプルアプリ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479957/450d213b-3a57-0687-13a8-7ad3467e083d.png">

また過去にflutter_riverpodやAsyncValueを使って同じような記事を何度か投稿しています。

[Riverpod+StateNotifier+freezed+RetrofitでQiitaの記事を取得する](https://qiita.com/toda-axiaworks/items/fa2f77562bb2c0b7a158)

[Qiitaクライアント開発でRiverpod AsyncValue使ってみた](https://qiita.com/toda-axiaworks/items/7950cd28c39de18f4ecc)

繰り返し同じような記事を書く理由として、Flutter自体やライブラリの成長が早く、過去の記事のままだと正常に動作しなくなっていることをコメント頂いたことが大きな理由です。

また私自身、flutter_riverpodは使用したことがあっても、hooks_riverpodは触ったことがなかったので、この期に挑戦してみました。

## 本記事の対象読者

本記事の対象読者としては、Flutterの状態管理のライブラリを使ったことがある方を想定しています。
そのため主に過去の記事との差分のみ記載するようにしたいです。

## Flutterバージョン

使用するFlutterや、主なライブラリのバージョン
Flutter: 2.2.1

```yaml
dependencies:
  hooks_riverpod: ^0.14.0+4
  flutter_hooks: ^0.17.0

  retrofit: ^2.0.0
  freezed_annotation: ^0.14.2
  webview_flutter: ^2.0.8
  state_notifier: ^0.7.0
  flutter_state_notifier: ^0.7.0

dev_dependencies:
  build_runner: ^2.0.4
  freezed: ^0.14.2
  retrofit_generator: ^2.0.0+1
  json_serializable: ^4.1.3
```

※Flutter自体やライブラリの将来のバージョンアップにより、動作しなくなる可能性があります。

## 過去の記事からの変更点

### freezedのクラスがabstractクラス必須でなくなった

以前はabstractクラスであることが必須でしたが、現在は必須でなくなっています。

またnull safety対応として、`user.dart`や`article_state.dart`のようにDefaultで初期値を設定しています。

```user.dart
@freezed
class User with _$User {
  factory User({
    @Default('') @JsonKey(name: 'profile_image_url') final String profileImageUrl,
  }) = _User;

  factory User.fromJson(Map<String, dynamic> json) =>
      _$UserFromJson(json);
}
```

```article_state.dart
@freezed
class ArticleState with _$ArticleState {
  const factory ArticleState({
    @Default(AsyncValue.loading()) AsyncValue<List<Article>> articles,
  }) = _ArticleState;
}
```

あるいは`article.dart`のようにrequiredで必須にしています。

```article.dart
@freezed
class Article with _$Article {
  factory Article({
    required String title,
    required String url,
    required User user,
  }) = _Article;

  factory Article.fromJson(Map<String, dynamic> json) =>
      _$ArticleFromJson(json);
}
```

### hooks_riverpodを使ってみた
過去記事ではflutter_riverpodを使用していましたが、今回はhooks_riverpodを使用しました。（同時にflutter_hooksを導入しています）
useProviderが使えるようになったことで、以前は、

```dart
final state = watch(articleProvider.state);
```

のように書いていたところを、

```dart
final state = useProvider(articleProvider);
```

と書けるようになりました。

### riverpodの変更点
以前は以下のようにarticleProviderを書いていました。

```dart
final articleProvider = StateNotifierProvider(
      (_) => ArticleStateNotifier(
        ArticleRepository(),
      ),
    );
```

riverpodの更新により、StateNotifierProviderの後に`<ArticleStateNotifier, ArticleState>`と明示することが必須になっています。

```dart
final articleProvider =
    StateNotifierProvider<ArticleStateNotifier, ArticleState>(
  (_) => ArticleStateNotifier(
    ArticleRepository(),
  ),
);
```

「hooks_riverpodを使ってみた」「riverpodの変更点」の修正を反映したarticle_screenは以下のようになります。

```article_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:qiita_sample/data/entities/qiita_info.dart';
import 'package:qiita_sample/screens/article/article_item.dart';
import 'package:qiita_sample/screens/article/article_repository.dart';
import 'package:qiita_sample/screens/article/article_state_notifier.dart';
import 'package:qiita_sample/screens/article_detail/article_detail_screen.dart';

import 'article_state.dart';

final articleProvider =
    StateNotifierProvider<ArticleStateNotifier, ArticleState>(
  (_) => ArticleStateNotifier(
    ArticleRepository(),
  ),
);

class ArticleScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text(
          'Qiita Sample',
        ),
        centerTitle: true,
      ),
      body: _List(),
    );
  }
}

class _List extends HookWidget {
  @override
  Widget build(BuildContext context) {
    // hooksを導入したことでuseProviderを使用できるようになりました
    final state = useProvider(articleProvider);
    // 今回からpull to refreshを追加
    return RefreshIndicator(
      child: state.articles.when(
        data: (articles) => ListView.builder(
          itemCount: articles.length,
          itemBuilder: (context, int position) => ArticleItem(
            qiitaInfo: articles[position],
            onArticleClicked: (qiitaInfo) => _openArticleWebPage(
              context,
              qiitaInfo,
            ),
          ),
        ),
        loading: () => Center(
          child: CircularProgressIndicator(),
        ),
        error: (_, __) => Center(
          child: Text('データの取得に失敗しました。'),
        ),
      ),
      onRefresh: () => getArticle(context),
    );
  }

  void _openArticleWebPage(
    BuildContext context,
    QiitaInfo qiitaInfo,
  ) {
    Navigator.of(context).push(
      MaterialPageRoute(
        builder: (context) => ArticleDetailScreen(
          qiitaInfo: qiitaInfo,
        ),
      ),
    );
  }

  Future<void> getArticle(BuildContext context) async {
    await context.read(articleProvider.notifier).getFlutterArticles();
  }
}
```

#### その他細かい修正
Flutterのバージョンを上げたためか、Androidの場合はminSdkVersionを19に修正する必要がある。
※6/24追記 厳密には新しいwebview_flutterを入れたため、でした。

```android/app/build.gradle
minSdkVersion 19
```

#### 追加要素
特に理由はないのですが、過去の記事には導入していなかったpull-to-refresh（記事のリストを下に引っ張ると再読み込み）による記事更新機能を追加しています。

### 課題など
- hooksを導入すると、riverpodが簡潔に書けるようになるのは少し体感できましたが、まだhooksらしい書き方が分かっていない

- null safety対応で`required`と`@Default`の使い分けが曖昧

- サンプルだと機能や画面が少なすぎて、まだそこまで使い心地が掴めてない
→ リリース中のproviderベースのアプリをriverpod_hooksにリプレイスしてみようかと考えている

## 今回のコード
https://github.com/todahiro/qiita_sample/tree/feature/riverpod_null_safety

## 最後に

相変わらずFlutterの成長が早く、正直2.0発表くらいから置いて行かれている感じがしていました。しかし今回ライブラリなどもようやくnull safety対応されているものも多かったり、本当にFlutter2.0にメジャーアップデートしたという感じがしています。

またFlutterの動向にも注目していきたいです。