経緯
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を叩く層
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
class ArticleRepository {
final _api = QiitaApiClient();
Future<List<QiitaArticle>> fetchArticles(String searchWord) async {
return await _api.fetchArticles(searchWord);
}
}
final repositoryProvider = Provider((ref) => ArticleRepository());
Model(freeze)
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);
}
@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);
}
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
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
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
を利用した時と同じように書けるため、馴染みやすかったです。
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の文字に依存してしまいますが、
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));