6/21追記 新しいFlutterやライブラリのバージョンに対応した記事を書きました。
【Flutter 2.2.1】hooks_riverpodやretrofit、freezedを使ってQiita APIから記事を取得
最初に-Riverpodについて
公式のドキュメント
https://riverpod.dev/
Riverpodに関してFlutter界隈では「RiverpodではなくProviderを使い続ける理由はほとんど無い」と言われているくらい今後メジャーになりそうなパッケージと言われています。
これまで私自身はProviderしか使ったことがなく、Riverpodは今回初めて使用しました。Providerでのコードと比べて簡潔に書けるようになった印象です。Providerを導入する時のコードの多さから何となく毎回重い腰を上げるような気持ちになります。
まだまだRiverpodについて分かっていない部分が多いですが、よろしくお願いします。
今回作成するもの
過去に2つほど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を追加します。
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のコードを整理と変更して、
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などを実装する場合に使用したいと思っています。)
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メソッドを使っていたので戸惑いましたが、コンストラクタに初期化コードを追加している方が多いようだったので私もそのように変更させて頂きました。
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の最新記事を取得出来ました。
まとめ
ひとまずRiverpodを使用して、Qiitaの記事が取得出来ることを確認出来ました。正直、自分の知識でRiverpodの深いところは全然解説したり出来ないので、ただの感想になってしまいますが、やはりProviderと比べてコードが書きやすいと思っています。
例えばProviderを使った時の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
以下のコマンドを実行して動作確認出来ます。
flutter packages pub run build_runner build
もう一度コマンドを実行し直す必要がある場合は以下のコマンドを実行して下さい。
flutter packages pub run build_runner build --delete-conflicting-outputs