はじめに
今回のサンプルの画面は以下になります。
また過去にflutter_riverpodやAsyncValueを使って同じような記事を何度か投稿しています。
Riverpod+StateNotifier+freezed+RetrofitでQiitaの記事を取得する
Qiitaクライアント開発でRiverpod AsyncValue使ってみた
繰り返し同じような記事を書く理由として、Flutter自体やライブラリの成長が早く、過去の記事のままだと正常に動作しなくなっていることをコメント頂いたことが大きな理由です。
また私自身、flutter_riverpodは使用したことがあっても、hooks_riverpodは触ったことがなかったので、この期に挑戦してみました。
本記事の対象読者
本記事の対象読者としては、Flutterの状態管理のライブラリを使ったことがある方を想定しています。
そのため主に過去の記事との差分のみ記載するようにしたいです。
Flutterバージョン
使用するFlutterや、主なライブラリのバージョン
Flutter: 2.2.1
dependencies:
hooks_riverpod: ^0.14.0+4
flutter_hooks: ^0.17.0
retrofit: ^2.0.0
freezed_annotation: ^0.14.2
webview_flutter: ^2.0.8
state_notifier: ^0.7.0
flutter_state_notifier: ^0.7.0
dev_dependencies:
build_runner: ^2.0.4
freezed: ^0.14.2
retrofit_generator: ^2.0.0+1
json_serializable: ^4.1.3
※Flutter自体やライブラリの将来のバージョンアップにより、動作しなくなる可能性があります。
過去の記事からの変更点
freezedのクラスがabstractクラス必須でなくなった
以前はabstractクラスであることが必須でしたが、現在は必須でなくなっています。
またnull safety対応として、user.dart
やarticle_state.dart
のようにDefaultで初期値を設定しています。
@freezed
class User with _$User {
factory User({
@Default('') @JsonKey(name: 'profile_image_url') final String profileImageUrl,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) =>
_$UserFromJson(json);
}
@freezed
class ArticleState with _$ArticleState {
const factory ArticleState({
@Default(AsyncValue.loading()) AsyncValue<List<Article>> articles,
}) = _ArticleState;
}
あるいはarticle.dart
のようにrequiredで必須にしています。
@freezed
class Article with _$Article {
factory Article({
required String title,
required String url,
required User user,
}) = _Article;
factory Article.fromJson(Map<String, dynamic> json) =>
_$ArticleFromJson(json);
}
hooks_riverpodを使ってみた
過去記事ではflutter_riverpodを使用していましたが、今回はhooks_riverpodを使用しました。(同時にflutter_hooksを導入しています)
useProviderが使えるようになったことで、以前は、
final state = watch(articleProvider.state);
のように書いていたところを、
final state = useProvider(articleProvider);
と書けるようになりました。
riverpodの変更点
以前は以下のようにarticleProviderを書いていました。
final articleProvider = StateNotifierProvider(
(_) => ArticleStateNotifier(
ArticleRepository(),
),
);
riverpodの更新により、StateNotifierProviderの後に<ArticleStateNotifier, ArticleState>
と明示することが必須になっています。
final articleProvider =
StateNotifierProvider<ArticleStateNotifier, ArticleState>(
(_) => ArticleStateNotifier(
ArticleRepository(),
),
);
「hooks_riverpodを使ってみた」「riverpodの変更点」の修正を反映したarticle_screenは以下のようになります。
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_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';
import 'article_state.dart';
final articleProvider =
StateNotifierProvider<ArticleStateNotifier, ArticleState>(
(_) => ArticleStateNotifier(
ArticleRepository(),
),
);
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 HookWidget {
@override
Widget build(BuildContext context) {
// hooksを導入したことでuseProviderを使用できるようになりました
final state = useProvider(articleProvider);
// 今回からpull to refreshを追加
return RefreshIndicator(
child: state.articles.when(
data: (articles) => ListView.builder(
itemCount: articles.length,
itemBuilder: (context, int position) => ArticleItem(
qiitaInfo: articles[position],
onArticleClicked: (qiitaInfo) => _openArticleWebPage(
context,
qiitaInfo,
),
),
),
loading: () => Center(
child: CircularProgressIndicator(),
),
error: (_, __) => Center(
child: Text('データの取得に失敗しました。'),
),
),
onRefresh: () => getArticle(context),
);
}
void _openArticleWebPage(
BuildContext context,
QiitaInfo qiitaInfo,
) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => ArticleDetailScreen(
qiitaInfo: qiitaInfo,
),
),
);
}
Future<void> getArticle(BuildContext context) async {
await context.read(articleProvider.notifier).getFlutterArticles();
}
}
その他細かい修正
Flutterのバージョンを上げたためか、Androidの場合はminSdkVersionを19に修正する必要がある。
※6/24追記 厳密には新しいwebview_flutterを入れたため、でした。
minSdkVersion 19
追加要素
特に理由はないのですが、過去の記事には導入していなかったpull-to-refresh(記事のリストを下に引っ張ると再読み込み)による記事更新機能を追加しています。
課題など
-
hooksを導入すると、riverpodが簡潔に書けるようになるのは少し体感できましたが、まだhooksらしい書き方が分かっていない
-
null safety対応で
required
と@Default
の使い分けが曖昧 -
サンプルだと機能や画面が少なすぎて、まだそこまで使い心地が掴めてない
→ リリース中のproviderベースのアプリをriverpod_hooksにリプレイスしてみようかと考えている
今回のコード
最後に
相変わらずFlutterの成長が早く、正直2.0発表くらいから置いて行かれている感じがしていました。しかし今回ライブラリなどもようやくnull safety対応されているものも多かったり、本当にFlutter2.0にメジャーアップデートしたという感じがしています。
またFlutterの動向にも注目していきたいです。