[Flutter + Riverpod + Freezed] 状態管理部分を少し考えてみた

# 経緯
freezed使ってみたいな。
(...検索中)
んー。APIModel作成するだけじゃ面白くないな。
(...検索中)
loadingとdataの状態を各々変更して、参照してか。。
状態管理なのだから`state`で「データがある」、「読み込んでる」、「エラー出てる」のシンプル構成にしたいな

# フォルダー構成
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185455/12b0eacd-fd23-33e8-d470-3539a230b47c.png", width=200>

- qiita_api_client.dart
    - APIを実際に叩くファイル
- qiita_article.dart, qiita_user.dart
    - 取得したAPIパースするための構造体
- article_state.dart
    - 表示するデータおよび状態を管理するファイル
- article_view_model.dart
    - 状態の変更や発行を担当
- *.g.dart, *.freezed.dart
    - 自動生成される部分なため特に気にすることはないファイル

# 処理の流れ
View > ViewModel > Repository > Client

## Client
実際にAPIを叩く層

```dart: qiita_api_client.dart

class QiitaApiClient {
  Future<List<QiitaArticle>> fetchArticles(String keyword) async {
    final response = await Dio().get(
      'https://qiita.com/api/v2/items',
      queryParameters: {
        'page': 1,
        if (keyword != '') 'query': 'body:$keyword or tag:$keyword',
      },
      options: Options(
        headers: {
          "Content-Type": "application/json",
          "Authorization":
              " Bearer [Access Token]", 
        },
      ),
    );
    var articles = List<QiitaArticle>.from(
        response.data.map((i) => QiitaArticle.fromJson(i)).toList());
    return articles;
  }
}
```

`Authorization`は個人のアクセストークンをご利用ください！
[参考記事](https://qiita.com/miyuki_samitani/items/bfe89abb039a07e652e6)

## Repository
```dart:article_repository.dart
class ArticleRepository {
  final _api = QiitaApiClient();

  Future<List<QiitaArticle>> fetchArticles(String searchWord) async {
    return await _api.fetchArticles(searchWord);
  }
}

final repositoryProvider = Provider((ref) => ArticleRepository());
```

# Model(freeze)
```dart: qiita_article.dart
part 'qiita_article.freezed.dart';
part 'qiita_article.g.dart';

@freezed
class QiitaArticle with _$QiitaArticle {
  factory QiitaArticle({
    String? title,
    String? url,
    QiitaUser? user,
    List? tags,
    @JsonKey(name: 'created_at') String? createdAt,
  }) = _QiitaArticle;

  factory QiitaArticle.fromJson(Map<String, dynamic> json) =>
      _$QiitaArticleFromJson(json);
}
```
```qiita_user.dart
@freezed
class QiitaUser with _$QiitaUser {
  factory QiitaUser({
    String? id,
    @JsonKey(name: 'profile_image_url') String? profileImageUrl,
  }) = _QiitaUser;

  factory QiitaUser.fromJson(Map<String, dynamic> json) =>
      _$QiitaUserFromJson(json);
}
```
```qiita_article_state.dart
part 'qiita_article_state.freezed.dart';

extension QiitaArticleGetters on QiitaArticleState {
  bool get isLoading => this is _QiitaArticleStateLoading;
}

@freezed
abstract class QiitaArticleState with _$QiitaArticleState {
  const factory QiitaArticleState.init() = _QiitaArticleStateInit;
  const factory QiitaArticleState.loading() = _QiitaArticleStateLoading;
  const factory QiitaArticleState.data(
      {required List<QiitaArticle> qiitaArticles}) = _QiitaArticleStateData;
  const factory QiitaArticleState.error([String? error]) =
      _QiitaArticleStateError;
}

```
freezedアノテーションをつけた3つを記載した後、
rootディレクトリでお馴染みの以下を実行
`flutter pub run build_runner build --delete-conflicting-outputs`

### 補足
`qiita_article_state.dart`で状態と保持するデータを定義
これをviewModelで操作し、viewに反映させる

## ViewModel

```dart:article_view_model.dart
class ArticleViewModel extends StateNotifier<QiitaArticleState> {
  ArticleViewModel(this._articleRepository)
      : super(const QiitaArticleState.init());　//ステータスを初期化に
  final ArticleRepository _articleRepository;

  fetchQiitaArticle(String searchWord) async {
    //ステータスを読み込み中に
    state = const QiitaArticleState.loading();

    try {
      final data = await _articleRepository.fetchArticles(searchWord);
      //ステータスを保持データに
      state = QiitaArticleState.data(qiitaArticles: data);
    } catch (_) {
      //ステータスをエラーに
      state = const QiitaArticleState.error('Error');
    }
  }
}
```

## View
```dart:qiita_list.dart
final textProvider = StateProvider<String>((ref) => 'fuga');

final qiitaAPINotifierProvider =
    StateNotifierProvider<ArticleViewModel, QiitaArticleState>((ref) {
  return ArticleViewModel(ref.watch(repositoryProvider));
});

class QiitaList extends ConsumerWidget {
  QiitaList({Key? key, required this.title}) : super(key: key);
  String title = '';

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final qiitaAPI = ref.watch(qiitaAPINotifierProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text(title),
      ),
      body: Column(
        children: [
          TextField(onChanged: (value) {
            final textController = ref.read(textProvider.notifier);
            textController.state = value;
          }),
          // ステータスに応じたViewを定義
          qiitaAPI.when(
            initial: () => const Text('検索ワードを入力して検索してください'),
            data: (list) => list.isNotEmpty
                ? Expanded(
                    child: ListView(
                        children:
                            list.map((e) => Text(e.title ?? 'hoge')).toList()),
                  )
                : const Text('list is empty'),
            error: (error) => Text(error.toString()),
            loading: () => const Center(child: CircularProgressIndicator()),
          ),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: list.isLoading  ? () {
          final qiitaAPIController = ref.read(qiitaAPINotifierProvider.notifier);
          qiitaAPIController.fetchQiitaArticle(ref.read(textProvider));
        } : null,
        tooltip: 'Search',
        child: const Icon(Icons.search),
      ),
    );
  }
}
```

# 最後に
いかがだったでしょうか？
個人的には以下の部分が`FutureProvider`を利用した時と同じように書けるため、馴染みやすかったです。
```dart: qiita_list.dart(抜粋)
qiitaAPI.when(
  initial: () => const Text('検索ワードを入力して検索してください'),
  data: (list) => list.isNotEmpty
      ? Expanded(
          child: ListView(
              children:
                  list.map((e) => Text(e.title ?? 'hoge')).toList()),
        )
      : const Text('list is empty'),
  error: (error) => Text(error.toString()),
  loading: () => const Center(child: CircularProgressIndicator()),
),
```

参考になれば幸いです！

# 追記
今回の内容とは軸がぶれますが、
検索ワードを検索ボタン押下時に引数渡しでAPIを叩きにいっている部分
RepositoryにReaderを定義し、取得することでも実装可能です。
ただし例ではviewのTextFieldの文字に依存してしまいますが、

```dart:article_repository.dart
class ArticleRepository {
  ArticleRepository(this.read);

  final _api = QiitaApiClient();
  final Reader read;

  Future<List<QiitaArticle>> fetchArticles(String searchWord) async {
    //TextFieldで入力した文字を取得
    String keyword = read(textProvider);
    return await _api.fetchArticles(keyword);
  }
}

final repositoryProvider = Provider((ref) => ArticleRepository(ref.read));
```