Riverpod+StateNotifier+freezed+RetrofitでQiitaの記事を取得する

6/21追記 新しいFlutterやライブラリのバージョンに対応した記事を書きました。
[【Flutter 2.2.1】hooks_riverpodやretrofit、freezedを使ってQiita APIから記事を取得](https://qiita.com/toda-axiaworks/items/fc49edb6ba67d572375b)

## 最初に-Riverpodについて

公式のドキュメント
https://riverpod.dev/

Riverpodに関してFlutter界隈では「RiverpodではなくProviderを使い続ける理由はほとんど無い」と言われているくらい今後メジャーになりそうなパッケージと言われています。

これまで私自身はProviderしか使ったことがなく、Riverpodは今回初めて使用しました。Providerでのコードと比べて簡潔に書けるようになった印象です。Providerを導入する時のコードの多さから何となく毎回重い腰を上げるような気持ちになります。

まだまだRiverpodについて分かっていない部分が多いですが、よろしくお願いします。

## 今回作成するもの

過去に２つほどQiitaの記事を取得するFlutterの記事を書いています。

- [FlutterでQiitaクライアント作成(Provider + freezed + Retrofit)]
(https://qiita.com/toda-axiaworks/items/0e172e7bb0c7551ce745)
（現在、ライブラリのバージョンが古く、動作しなくなっています）

- [FlutterでQiitaクライアント作成(Provider+StateNotifier+freezed+Retrofit)]
(https://qiita.com/toda-axiaworks/items/43f9aeaf7556c8c27d66)

当然今回はRiverpodを使用する点が、過去の記事と異なります。freezedやStateNotifierは引き続き使っていきます。

またコードは過去の記事のものを改良する形で進めます。この記事ではStateNotifierやfreezedの部分のコードなどは割愛しています。

## Riverpodの導入

公式のドキュメントにも記載されていますが、Riverpodを導入するにあたり、いくつかライブラリの種類があります。今回はflutter_hooksなどは使わないため、flutter_riverpodを追加します。

```pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^0.12.4 // Riverpodの導入
  retrofit: ^1.3.4+1
  freezed_annotation: ^0.12.0
  logging: ^0.11.4
  pretty_dio_logger: ^1.1.1
  webview_flutter: ^0.3.22+1
  state_notifier: ^0.6.0
  flutter_state_notifier: ^0.6.1

  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^0.1.3

dev_dependencies:
  flutter_test:
    sdk: flutter
  # API access
  build_runner: ^1.10.7
  freezed: ^0.12.3
  retrofit_generator: ^1.4.0+2
  json_serializable: ^3.5.1

// (以下略)

```

忘れないように```pub get```を実行します。

## Provider用のコードからRiverpod対応コードに変更

元のプロジェクトから今回、変更する必要があるファイルはmain.dart, article_screen.dartなどです。

まずProvider用のコードを削除したりmain.dartのコードを整理と変更して、

```main.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:qiita_sample/screens/article/article_screen.dart';

void main() => runApp(
      ProviderScope(
        child: MyApp(), // ProviderScopeで囲む
      ),
    );

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Qiita Sample',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: ArticleScreen(),
    );
  }
}
```

article_screen.dartが今回は比較的大きく変わります。Riverpodを導入するにあたり、いくつか選択肢がありました。今回は一番影響範囲が少ない（と思っている）`Consumer` というWidgetを使用します。

ちなみに他の選択肢としてはStatelessWidgetを`ConsumerWidget`に変更するというものがあります。
（StatefulWidetを継承しているためpull-to-refreshなどを実装する場合に使用したいと思っています。）

```article_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_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';

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 StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final articleProvider = StateNotifierProvider(
      (_) => ArticleStateNotifier(
        ArticleRepository(),
      ),
    );
    // この辺がProviderの時よりも、シンプルに記述出来るようになったと感じています
    return Consumer( 
      builder: (context, watch, child) {
        final state = watch(articleProvider.state);
        return ListView.builder(
          itemCount: state.articles.length,
          itemBuilder: (context, int position) => ArticleItem(
            qiitaInfo: state.articles[position],
            onArticleClicked: (qiitaInfo) => _openArticleWebPage(
              context,
              qiitaInfo,
            ),
          ),
        );
      },
    );
  }

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

ひとまずここまでの変更がメインになります。

が、この変更だけでビルドして実行すると、起動後に記事を取得してくれません。

## 問題点 - ArticleStateNotifierのinitStateが動作していない

ビルドエラーなどにならないため気づかなかったのですが、ProviderからRiverpodに変更した影響で、LocatorMixinのinitStateが動作しなくなっています。(今回かなり驚きました。)

StateNotifierでは、ほぼ毎回initStateメソッドを使っていたので戸惑いましたが、コンストラクタに初期化コードを追加している方が多いようだったので私もそのように変更させて頂きました。

```article_state_notifier.dart
import 'package:qiita_sample/screens/article/article_repository.dart';
import 'package:state_notifier/state_notifier.dart';

import 'article_state.dart';

class ArticleStateNotifier extends StateNotifier<ArticleState> {
  ArticleStateNotifier(this.repository) : super(ArticleState()) {
    _getFlutterArticles();
  }

  final ArticleRepository repository;

  Future<void> _getFlutterArticles() async {
    var flutterArticles = await repository.getFlutterArticles();
    state = state.copyWith(
      articles: flutterArticles,
    );
  }
}
```

## 動作確認

Riverpodを使用して同じようにQiitaのFlutterの最新記事を取得出来ました。

![スクリーンショット 2020-12-06 22.51.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/479957/13a404ab-4962-5e76-a3d2-993e9b2ace63.png)

## まとめ

ひとまずRiverpodを使用して、Qiitaの記事が取得出来ることを確認出来ました。正直、自分の知識でRiverpodの深いところは全然解説したり出来ないので、ただの感想になってしまいますが、やはりProviderと比べてコードが書きやすいと思っています。

例えばProviderを使った時のarticle_screen.dartは以下のようになっていました。

```article_screen.dart
// providerバージョン
class ArticleScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // この辺が書きづらい気がしています
    return StateNotifierProvider<ArticleStateNotifier, ArticleState>(
      create: (_) {
        return ArticleStateNotifier(
          ArticleRepository(),
        );
      },
      builder: (_, __) => Scaffold(
        appBar: AppBar(
          title: const Text(
            'Qiita Sample',
          ),
          centerTitle: true,
        ),
        body: _List(),
      ),
    );
  }
}

class _List extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final state = context.watch<ArticleState>();
    return ListView.builder(
      itemCount: state.articles.length,
      itemBuilder: (context, int position) => ArticleItem(
        qiitaInfo: state.articles[position],
        onArticleClicked: (qiitaInfo) => _openArticleWebPage(
          context,
          qiitaInfo,
        ),
      ),
    );
  }
// (以下略)
```

Providerに対する理解が浅いからかもしれませんが、一部呪文のようなコードを書いている気持ちになっていました。Riverpodの場合の方が私にとってはとても書きやすく感じています。

まだ新しいパッケージなので破壊的変更が入ったりするかもしれませんが、Flutter自体進化が早くキャッチアップが必要な技術だと思っているので、ある意味どれを使っても大変だと思っています。

しばらく特に自分の趣味のコードでは、Riverpodをメインに使ってみようと思います。

時間があれば今回のコードに
- 読み込み中のインジケーターを追加
- ConsumerWidgetを使ってpull-to-refreshの実装

あたりを追加したりしたいです。

## 今回のコード

現在私のFlutterバージョンは1.22.4です。

今回のコードは以下になります。
https://github.com/toda-axia/qiita_sample/tree/feature/riverpod

以下のコマンドを実行して動作確認出来ます。

```zsh
flutter packages pub run build_runner build
```

もう一度コマンドを実行し直す必要がある場合は以下のコマンドを実行して下さい。

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