0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-05-23

経緯

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

フォルダー構成

  • 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を叩く層

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は個人のアクセストークンをご利用ください!
参考記事

Repository

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)

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

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

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を利用した時と同じように書けるため、馴染みやすかったです。

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の文字に依存してしまいますが、

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));
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?