0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

posted at

updated at

Organization

FlutterでQiitaクライアント作成(Provider+StateNotifier+freezed+Retrofit)

前回の振り返り

先日、上記のような記事を作成しました。

今回はそのPart2ということで、前回の記事に書いた通り、StateNotifierを使って前回のサンプルをアップデートしたいと思います。

※注意書きとして、StateNotifierのサンプルが少なく、私自身調べながら今回のサンプルをなんとか作成したというレベルなので、誤っている部分やもっと良い方法もあるかと思います。

[補足]
前回は社内のハンズオンのため省略したrepository層が、今回から追加されています。

article_repository.dart

class ArticleRepository {
  final _api = QiitaApiClient.create();
  Future<List<QiitaInfo>> getFlutterArticles() async {
    return await _api.getFlutterArticles();
  }
}

今回やりたいこと

かなり曖昧だったStateNotifierの理解を深めるため、作成する成果物は前回と同様のものです。
記事の一覧を表示して、

スクリーンショット 2020-09-14 0.52.13.png

記事をタップすると、記事の詳細が見れるというシンプルなアプリです。

前回は使用しなかったStateNotifierを使用して作り直します。

今回のコードの共有

Flutterのバージョン 1.22.4
Dartのバージョン 2.10.4

前回同様、始めにコードを共有させて頂きます。前回と同じプロジェクトのstate_notifier_publicというブランチになります。

前回からの大きな変更点としては、article_screen_model.dartというファイルが削除されて、article_state.dartとarticle_state_notifier.dartというファイルが新たに追加されました。

Part1と同じく、以下のコマンドを実行してから、

flutter packages pub run build_runner build

コマンドの2回目以降は以下のコマンドを実行してからアプリ起動して下さい。

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

ライブラリのインストール

まず初めに今回のサンプルのために、

pubspec.yamlファイルのdependenciesに以下を追加して下さい。

pubspec.yaml
state_notifier: ^0.6.0
flutter_state_notifier: ^0.6.1

私のStateNotifierの理解

まず前回のコードと比較して明らかに分かるところですが、上にも書いた通り、Part1ではArticleScreenModelとして存在していたものが削除され、ArticleStateとArticleStateNotifierが新たに追加されたことからも、

ArticleScreenModelの役割を、ArticleStateとArticleStateNotifierに分割したようなイメージでいます。

では、そのPart1のArticleScreenModelとはどんな役割だったか?ということを考えると、QiitaのAPIから記事の一覧情報を取得して、その一覧データを取得した時にデータの変更を通知する、というようなことをしていました。

ではその役割を分割する場合に、それぞれの役割を書くと(自分のざっくり理解です)、

  • ArticleState  => 取得する記事のデータの入れ物
  • ArticleStateNotifier => 記事を取得した際にArticleStateの入れ物にデータを入れ、その変更を通知する

のように役割が分かれていると思っています。

それぞれのコードは以下のようになりました。

article_state.dart

@freezed
abstract class ArticleState with _$ArticleState {
  const factory ArticleState({@Default([]) List<QiitaInfo> articles}) = _ArticleState;

  factory ArticleState.fromJson(Map<String, dynamic> json) =>
      _$ArticleStateFromJson(json);
}

article_state_notifier.dart
class ArticleStateNotifier extends StateNotifier<ArticleState> with LocatorMixin {
  ArticleStateNotifier(this.repository)
      : super(
          const ArticleState(),
        );
  final ArticleRepository repository;

  @override
  void initState() {
    super.initState();

    _getFlutterArticles();
  }

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

ArticleStateで入れ物を作って、(@Defaultは初期値を設定しています。)

ArticleStateNotifierで取得した記事一覧情報をArticleStateのarticlesにセットしている。LocatorMixinを導入することで、initStateという初期化メソッドを使用出来ます。

また作成したArticleStateとArticleStateNotifierを実際にどのように使うかというと、前回のコードからなるべく大きく変更しないようにして以下のようになりました。

article_screen.dart
import 'package:flutter/material.dart';
import 'package:flutter_state_notifier/flutter_state_notifier.dart';
import 'package:provider/provider.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.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 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,
        ),
      ),
    );
  }

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

動作確認

動作確認としては、Part1と同じになるので、今回は省略します。

記事一覧が取得されて、各記事をタップすると記事詳細画面が表示されます。

まとめ

今回コードとしては割とコンパクトにまとまりましたが、自分としてはStateNotifierを使って今回のサンプルを作るところに到達するまでにはかなりの時間を要しました。

また前回に引き続き、間違っている表現やコードもあるかと思いますので、引き続き、ご指摘お願いします。

まだまだFlutterそのものの日本語記事や書籍など、他の情報が豊富な技術と比べると多くはなく、自分のようにStateNotifierなどをドキュメントを見ただけでは簡単には理解出来ず、「もう少し動くサンプルがあれば...」という方の役にたてば幸いだと思っています。

自分でも気づいたことがあれば随時修正します。

(2020/12/06追記)
ライブラリが古く、build_runnerが延々続くようになっていたのでライブラリのアップデートと、ProviderやStateNotifierを使ううちに色々気づいたことがあったのでコードを修正しました。

参考記事

今回は本当に色々な方の記事を参考にさせて頂きました。特に参考にさせて頂いたものを参考記事として共有させて頂きます。

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
0
Help us understand the problem. What are the problem?