api取得して状態管理をStateNotifierとAsyncNotifierで比較
お久しぶりです!
個人開発しているアプリで、Riverpodを使っているのですが
何となくRiverpod使ってみる→→→なんかできた😅
「とりあえず動かしてみる」状態だったのでもっと理解を深めるために学習しています
よろしくお願いします。
アプリ
楽天ブックス総合検索APIを使って情報を取得し、それをGridViewを表示させる
パッケージ
パッケージ
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
riverpod: ^2.5.0
flutter_riverpod: ^2.4.10
dio: ^5.4.0
freezed:
build_runner:
riverpod_annotation: ^2.3.4
riverpod_generator: ^2.3.11
dev_dependencies:
flutter_test:
sdk: flutter
json_serializable:
API取得
取得するApi情報
jsonファイル
{
"GenreInformation": [],
"Items": [
{
"Item": {
"affiliateUrl": "",
"artistName": "",
"author": "尾田 栄一郎",
"availability": "5",
"booksGenreId": "001001001008",
"chirayomiUrl": "",
"discountPrice": 0,
"discountRate": 0,
"hardware": "",
"isbn": "9784088840130",
"itemCaption": "",
"itemPrice": 528,
"itemUrl": "https://books.rakuten.co.jp/rb/17764849/",
"jan": "",
"label": "",
"largeImageUrl": "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/0130/9784088840130.gif?_ex=200x200",
"limitedFlag": 0,
"listPrice": 0,
"mediumImageUrl": "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/0130/9784088840130.gif?_ex=120x120",
"os": "",
"postageFlag": 2,
"publisherName": "集英社",
"reviewAverage": "0.0",
"reviewCount": 0,
"salesDate": "2024年03月04日",
"smallImageUrl": "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/0130/9784088840130.gif?_ex=64x64",
"title": "ONE PIECE 108"
}
},
{
"Item": {
"affiliateUrl": "",
"artistName": "",
"author": "原 泰久",
"availability": "5",
"booksGenreId": "001001003005",
"chirayomiUrl": "",
"discountPrice": 0,
"discountRate": 0,
"hardware": "",
"isbn": "9784088931197",
"itemCaption": "",
"itemPrice": 715,
"itemUrl": "https://books.rakuten.co.jp/rb/17739270/",
"jan": "",
"label": "",
"largeImageUrl": "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/1197/9784088931197_1_3.jpg?_ex=200x200",
"limitedFlag": 0,
"listPrice": 0,
"mediumImageUrl": "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/1197/9784088931197_1_3.jpg?_ex=120x120",
"os": "",
"postageFlag": 2,
"publisherName": "集英社",
"reviewAverage": "5.0",
"reviewCount": 4,
"salesDate": "2024年02月19日",
"smallImageUrl": "https://thumbnail.image.rakuten.co.jp/@0_mall/book/cabinet/1197/9784088931197_1_3.jpg?_ex=64x64",
"title": "キングダム 71"
}
}
],
"carrier": 0,
"count": 1044296,
"first": 1,
"hits": 2,
"last": 2,
"page": 1,
"pageCount": 100
Api取得準備
今回取得するのは、本のタイトル、画像、id,です
idと画像はJSONの'isbn'と'largeImageUrl'をそれぞれマッピングします
import 'package:freezed_annotation/freezed_annotation.dart';
part 'book.freezed.dart';
part 'book.g.dart';
class Book with _$Book {
const factory Book({
required String title,
(name: 'isbn') required String id,
(name: 'largeImageUrl') required String imageUrl,
}) = _Book;
factory Book.fromJson(Map<String, dynamic> json) => _$BookFromJson(json);
}
WishlistStateクラス定義
- books: List<Book>型で、Apiから取得した書籍のリストを保持します。デフォルト値は空のリスト[]です。
import 'package:api_riverpod/wishlist.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'wishlist_state.freezed.dart';
class WishlistState with _$WishlistState {
const factory WishlistState({
([]) List<Book> books,
}) = _WishlistState;
}
Apiから書籍データを取得
APIを通じて書籍データを取得するためのリポジトリクラス(WishlistRepository)を定義しています。
ここでは、Riverpodを使用して状態管理を行い、Dioライブラリを使ってHTTPリクエストを送信しています。それぞれの部分について詳しく解説します。
import 'package:api_riverpod/wishlist.dart';
import 'package:dio/dio.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
final repositoryProvider = Provider.family<WishlistRepository, String>(
(_, applicationId) => WishlistRepository(applicationId),
);
class WishlistRepository {
WishlistRepository(this.applicationId);
final String applicationId;
//APIリクエストをする準備
Dio get _client => Dio(
BaseOptions(
baseUrl: "https://app.rakuten.co.jp/services/api",
queryParameters: {"applicationId": applicationId},
),
);
// 書籍データを非同期に取得するメソッド
Future<List<Book>> getBooks() async {
// APIからデータを取得
final result = await _client.get(
"/BooksTotal/Search/20170404?&keyword=$本&NGKeyword=予約&sort=sales&page=1");
// 取得したデータから"Items"キーに対応する部分を抽出
final List<dynamic> items = result.data["Items"];
// 抽出した各アイテムをBookオブジェクトに変換し、List<Book>として返します。
// ここで、itemMap['Item']をMap<String, dynamic>型にキャストして、fromJsonコンストラクタに渡しています。
return items
.map<Book>(
(itemMap) => Book.fromJson(itemMap['Item'] as Map<String, dynamic>))
.toList();
}
}
- repositoryProvider
の定義
WishlistRepository
のインスタンスをアプリケーション全体で再利用できます。
Provider.family
を使用することで、applicationId
を引数として受け取ることができる。
- WishlistRepository
クラス
このクラスでは、特定のAPIから書籍データを取得する機能を提供します。
applicationId
を受け取り、それを使用してAPIリクエストに必要なパラメータを設定します。
- Dio get _client
APIのURL(baseUrl)と、クエリパラメータ(applicationId)を用いてAPI通信するための基本設定を行います。
- baseUrl
APIリクエストを送信する際の基本となるURL
- queryParameters
URLのクエリ部分(?key=valueの形式)に相当するパラメータ
- getBooks
メソッド
非同期に書籍データを取得します
APIからのレスポンス内の"Items"キーに対応する部分を抽出し、それをBookオブジェクトのリストに変換しています。
riverpodを使ってapiデータを状態管理する
AsyncNotifier
非同期処理の結果に特化しており、APIからのデータ取得など非同期操作の状態管理に適しています。
AsyncNotifierを使用すると、データの読み込み中、読み込み成功、読み込み失敗の各状態を簡単に管理できます。
状態管理 WishlistAsyncNotifierクラス
import 'package:api_riverpod/api.dart';
import 'package:api_riverpod/wishlist.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'async_notifier.g.dart';
class WishlistAsyncNotifier extends _$WishlistAsyncNotifier {
WishlistRepository get _api => ref.read(repositoryProvider(ApplicationId));
Future<WishlistState> build() => _loadBooks();
// 書籍データを読み込み
Future<WishlistState> _loadBooks() async {
final response = await _api.getBooks();
return WishlistState(books: response);
}
// 書籍データを再読み込み
Future<void> reloadBooks() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() => _loadBooks());
}
}
このコードでは、WishlistRepositoryを通じてAPIから書籍データを非同期に取得し、WishlistStateにそのデータを格納しています。
riverpod_annotationを使用することで、状態管理のコードが自動生成され、Riverpodの機能を手軽に活用できます。
- WishlistRepository get _api
:
WishlistRepository
のインスタンスを取得し、APIから書籍データを取得できるようになります
- build
: ここが最初に呼び出されます。
_loadBooksを呼び出して書籍データの初期読み込みを行います。
- _loadBooks
: 実際にAPIを呼び出して書籍データを取得し、WishlistStateクラスのbooksに取得したデータを入れます
- AsyncValue
: 同期処理の結果をloading、data、errorの3つの状態で管理するための便利なユーティリティです。AsyncValue.guardを使用すると、非同期処理が自動的にこれらの状態に変換され、例外処理も内部で行われます。これにより、UI側での状態管理が非常にシンプルになります。
- reloadBooksメソッド
: 書籍データを再度読み込むためのメソッドです。
最初にAsyncValue.loading()
を使って状態を読み込み中に設定し、_loadBooksメソッドを呼び出してデータの再読み込みを行います。
view
import 'package:api_riverpod/providers/async_notifier.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class WishlistAsyncNotifierApp extends ConsumerStatefulWidget {
const WishlistAsyncNotifierApp({required this.title, super.key});
final String title;
ConsumerState<WishlistAsyncNotifierApp> createState() =>
_WishlistAsyncNotifierAppState();
}
Widget build(BuildContext context) {
final state = ref.watch(wishlistAsyncNotifierProvider);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: state.when(
error: (e, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Problem loading books"),
SizedBox(
height: 10,
),
OutlinedButton(
onPressed: () {
//再読み込み
ref
.read(wishlistAsyncNotifierProvider.notifier)
.reloadBooks();
},
child: Text("Try again"))
],
),
),
loading: () => Center(
child: CircularProgressIndicator(),
),
data: (data) => GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: data.books.length,
itemBuilder: (context, index) {
final book = data.books[index];
return Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Positioned.fill(
child: Image.network(
book.imageUrl,
fit: BoxFit.fitHeight,
),
),
Positioned.fill(
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.grey,
],
),
),
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
data.books[index].title,
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
);
},
),
),
);
}
- ref.watch(wishlistAsyncNotifierProvider)
を使用して、wishlistAsyncNotifierProviderの現在の状態を監視します。
- state.whenメソッドを使って、非同期処理の結果に応じた異なるUIを表示します:
- loading: 読み込み中はCircularProgressIndicatorを表示して、ユーザーにデータがロード中であることを伝えます。
- data: データが正常に読み込まれた場合、GridView.builderを使用して書籍のデータをグリッド形式で表示します。各書籍はCardウィジェットで表示され、書籍の画像とタイトルが含まれます。
- error: エラーが発生した場合は、エラーメッセージと「再試行」ボタンを表示します。このボタンを押すと、reloadBooks
メソッドが呼び出され、データの再読み込みが行われます。
StateNotifier
一般的な状態管理に適しており、状態の変更を効率的にリスナーに通知します。非同期処理の結果だけでなく、任意の状態の管理に使用できます。
状態管理 WishlistStateNotifierクラス
非同期処理の結果を管理
enum LoadingState { progress, success, error }
- progress:読み込み中
- success:成功
- error:失敗
WishlistStateクラス
デフォルトはprogressに設定
import 'package:api_riverpod/wishlist.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'wishlist_state.freezed.dart';
@freezed
class WishlistState with _$WishlistState {
const factory WishlistState({
([]) List<Book> books,
+ (LoadingState.progress) LoadingState loading,
}) = _WishlistState;
}
WishlistStateNotifierクラス
// WishlistStateNotifierクラス
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api.dart'; // API通信を行うためのクラスをインポート
import '../wishlist.dart'; // Wishlist関連のデータ構造を定義したファイルをインポート
// Wishlistの状態を管理するStateNotifierProvider
final wishlistStateProvider = StateNotifierProvider<WishlistStateNotifier, WishlistState>((ref) => WishlistStateNotifier(ref));
class WishlistStateNotifier extends StateNotifier<WishlistState> {
// コンストラクタ: Refを使用してWishlistRepositoryのインスタンスを取得し、初期状態を設定
WishlistStateNotifier(Ref ref)
: _api = ref.read(repositoryProvider(ApplicationId)),
super(const WishlistState());
final WishlistRepository _api; // APIとの通信を担当するWishlistRepositoryのインスタンス
// 書籍データを非同期に読み込むメソッド
Future<void> loadBooks() async {
try {
// APIから書籍データを非同期に取得
final response = await _api.getBooks();
// 取得成功: 取得したデータで状態を更新
state = state.copyWith(books: response, loading: LoadingState.success);
} catch (_) {
// 取得失敗: 読み込み状態をerrorに更新
state = state.copyWith(loading: LoadingState.error);
}
}
// 書籍データを再読み込みするメソッド
Future<void> reloadBooks() async {
// 読み込み状態をprogressに設定してから、書籍データの読み込みを開始
state = state.copyWith(loading: LoadingState.progress);
await loadBooks(); // ここでawaitを使うことで、非同期処理の完了を待つ
}
}
- wishlistStateProvider
StateNotifierProviderを使用してWishlistStateNotifierのインスタンスを作成します。これでアプリケーションのどこからでもアクセス可能な状態を提供します。
WishlistState型の状態を持ち、この状態をアプリケーション全体で共有および更新できるようにします。
- コンストラクタと初期化
WishlistStateNotifier
のコンストラクタは、APIから書籍データを取得するために、
Refオブジェクトを引数に取り、repositoryProviderを通じてWishlistRepositoryのインスタンスを取得します。
初期状態としてconst WishlistState()を設定しています。
View
import 'package:api_riverpod/providers/state_notifier.dart';
import 'package:api_riverpod/wishlist.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class WishlistStateNotifierApp extends ConsumerStatefulWidget {
const WishlistStateNotifierApp({required this.title, super.key});
final String title;
ConsumerState<WishlistStateNotifierApp> createState() =>
_WishlistCNProviderAppState();
}
class _WishlistCNProviderAppState
extends ConsumerState<WishlistStateNotifierApp> {
void initState() {
super.initState();
//データのロード
ref.read(wishlistStateProvider.notifier).loadBooks();
}
Widget build(BuildContext context) {
final wishlist = ref.watch(wishlistStateProvider);
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: wishlist.loading == LoadingState.progress
? const Center(
//読み込み中
child: CircularProgressIndicator(),
)
: wishlist.loading == LoadingState.error
//エラー処理
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text("Problem loading books"),
SizedBox(
height: 10,
),
OutlinedButton(
onPressed: () {
// 書籍データを再読み込みする
ref
.read(wishlistStateProvider.notifier)
.reloadBooks();
},
child: Text("Try again"))
],
),
)
//成功
: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
),
itemCount: wishlist.books.length,
itemBuilder: (context, index) {
final book = wishlist.books[index];
return Card(
clipBehavior: Clip.antiAlias,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
child: Stack(
children: [
Positioned.fill(
child: Image.network(
book.imageUrl,
fit: BoxFit.fitHeight,
),
),
Positioned.fill(
child: Container(
padding: EdgeInsets.all(10),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.transparent,
Colors.grey,
],
),
),
child: Align(
alignment: Alignment.bottomLeft,
child: Text(
wishlist.books[index].title,
style: TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
);
},
),
);
}
}
- AsyncNotifierは最初に
.when
でapiからデータ取得するが、
今回は.when
が使えないので、書く必要があります一番初めに書籍データ取得を実行する必要がある
ConsumerStatefulのinitStateでloadBooks
を呼ぶ。 - 非同期処理の格状態に応じたUIの更新もif文で書く必要がある
まとめ
-
StateNotifierの特徴
- 汎用性: 同期および非同期の状態管理に使用できる。
- 明示的な状態管理: 状態の変更が明示的に行われ、状態の更新ロジックが開発者によって完全にコントロールされる。
-
AsyncValueの特徴
- 非同期処理の簡素化: 非同期処理の結果を簡単に管理できる。loading、data、errorの状態を自動的に扱う。
- UIとの統合: .when()メソッドを使用して、非同期処理の各状態に基づいたUIの更新を簡潔に記述できる。
- エラーハンドリング: 非同期処理中に発生したエラーを捕捉し、それに応じた状態を自動的に管理する。
最後に
私は非同期処理に関して、AsyncNotifierの利用が特に便利だと感じています。.whenメソッドが便利すぎる!
現在、Riverpod2におけるNotifierとAsyncNotifierの理解が状態管理の鍵とされています。しかし、すべてのプロジェクトがRiverpod2を採用しているわけではありません。そのため、StateNotifierを用いたコードとの比較をこの記事で行いました。
学習を始めたばかりで未経験の立場からすると、間違いを含む可能性があることを承知しています。もし誤りがあれば、正しい情報を優しく指摘していただけると幸いです。
今回学んだ書籍データを表示するアプリに、さらに多くの機能を追加しながら、状態管理の知識を深めていく予定です。
次はその記事を書く予定です!
最後までお読みいただき、ありがとうございました。
Discussion