Open9
riverpodで初学者が躓くポイント(Claude出力)
Riverpodで初学者がよく陥る実装の落とし穴と解決方法
1. Providerの過剰な作成
❌ よくある間違い
// 各状態ごとに個別のProviderを作成
final nameProvider = StateProvider((ref) => '');
final ageProvider = StateProvider((ref) => 0);
final addressProvider = StateProvider((ref) => '');
// これらを使用するWidget
class UserProfileWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final name = ref.watch(nameProvider);
final age = ref.watch(ageProvider);
final address = ref.watch(addressProvider);
return Column(
children: [
Text('Name: $name'),
Text('Age: $age'),
Text('Address: $address'),
],
);
}
}
✅ 推奨される実装
// ユーザー情報を一つのクラスにまとめる
class UserState {
final String name;
final int age;
final String address;
UserState({
required this.name,
required this.age,
required this.address,
});
UserState copyWith({
String? name,
int? age,
String? address,
}) {
return UserState(
name: name ?? this.name,
age: age ?? this.age,
address: address ?? this.address,
);
}
}
// 一つのProviderにまとめる
final userProvider = StateNotifierProvider<UserNotifier, UserState>((ref) {
return UserNotifier();
});
class UserNotifier extends StateNotifier<UserState> {
UserNotifier()
: super(UserState(name: '', age: 0, address: ''));
void updateName(String name) {
state = state.copyWith(name: name);
}
void updateAge(int age) {
state = state.copyWith(age: age);
}
void updateAddress(String address) {
state = state.copyWith(address: address);
}
}
2. watchとreadの誤用
❌ よくある間違い
class ProductListWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// buildメソッド内でreadを使用
final products = ref.read(productsProvider);
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return Text(products[index].name);
},
);
}
}
✅ 推奨される実装
class ProductListWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// watchを使用して値の変更を監視
final products = ref.watch(productsProvider);
return ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
return Text(products[index].name);
},
);
}
}
// ボタンクリックなどのイベントハンドラでは read を使用
void onButtonPressed(WidgetRef ref) {
// readを使用して値を一度だけ取得
final products = ref.read(productsProvider.notifier).addProduct();
}
3. 非同期処理の不適切な実装
❌ よくある間違い
final dataProvider = StateProvider<List<String>>((ref) => []);
class DataWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return FutureBuilder(
future: fetchData(), // 直接Future を使用
builder: (context, snapshot) {
if (snapshot.hasData) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return Text(data[index]);
},
);
}
return CircularProgressIndicator();
},
);
}
}
✅ 推奨される実装
final dataProvider = FutureProvider<List<String>>((ref) async {
// 非同期処理をProvider内で実装
return await fetchData();
});
class DataWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final asyncData = ref.watch(dataProvider);
return asyncData.when(
data: (data) {
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) {
return Text(data[index]);
},
);
},
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
}
4. 依存関係の誤った管理
❌ よくある間違い
final userProvider = StateProvider<User>((ref) => User());
final cartProvider = StateProvider<Cart>((ref) {
// 直接別のProviderの値を参照
final user = ref.read(userProvider);
return Cart(userId: user.id);
});
✅ 推奨される実装
final userProvider = StateProvider<User>((ref) => User());
final cartProvider = StateProvider<Cart>((ref) {
// watchを使用して依存関係を適切に管理
final user = ref.watch(userProvider);
return Cart(userId: user.id);
});
5. dispose処理の忘れ
❌ よくある間違い
final streamProvider = StreamProvider<int>((ref) {
final controller = StreamController<int>();
// disposeの処理を忘れている
return controller.stream;
});
✅ 推奨される実装
final streamProvider = StreamProvider<int>((ref) {
final controller = StreamController<int>();
ref.onDispose(() {
// 適切にリソースを解放
controller.close();
});
return controller.stream;
});
まとめ
- 状態管理は適切な粒度でまとめる
- watchとreadは用途に応じて使い分ける
- 非同期処理はFutureProvider/StreamProviderを活用する
- Provider間の依存関係は適切に管理する
- リソースの解放を忘れない
これらのポイントを意識することで、より保守性が高く、パフォーマンスの良いアプリケーションを開発することができます。
Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート2
1. ConsumerWidgetの過剰な使用
❌ よくある間違い
// 全てのWidgetをConsumerWidgetにしてしまう
class ParentWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Column(
children: [
ChildWidget1(),
ChildWidget2(),
ChildWidget3(),
],
);
}
}
class ChildWidget1 extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Text('Child 1');
}
}
class ChildWidget2 extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return Text('Child 2');
}
}
✅ 推奨される実装
// 必要なWidgetのみConsumerWidgetにする
class ParentWidget extends StatelessWidget {
Widget build(BuildContext context) {
return Column(
children: [
ChildWidget1(),
ChildWidget2(),
StateConsumingWidget(),
],
);
}
}
// 状態を使用するWidgetのみConsumerWidgetを使用
class StateConsumingWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(someProvider);
return Text('State: $state');
}
}
2. select修飾子の未使用
❌ よくある間違い
class UserProfileWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 必要のない再ビルドが発生する
final user = ref.watch(userProvider);
return Text(user.name); // 名前だけを表示したいのに全状態を監視
}
}
✅ 推奨される実装
class UserProfileWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 必要な値のみを監視
final userName = ref.watch(userProvider.select((user) => user.name));
return Text(userName);
}
}
3. StateNotifierの状態更新の誤り
❌ よくある間違い
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() {
// 直接状態を変更
state++;
}
void updateValue(int newValue) {
if (state == newValue) {
state = newValue; // 同じ値でも更新してしまう
}
}
}
✅ 推奨される実装
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() {
state = state + 1; // 新しい値を代入
}
void updateValue(int newValue) {
if (state != newValue) {
state = newValue; // 値が異なる場合のみ更新
}
}
}
4. autoDisposeの適切な使用
❌ よくある間違い
// 必要のないautoDisposeの使用
final constantDataProvider = Provider.autoDispose<String>((ref) {
return 'Constant Value'; // 定数値なのにautoDisposeを使用
});
// autoDisposeが必要な場面で使用していない
final searchResultProvider = FutureProvider<List<String>>((ref) async {
return await searchAPI();
});
✅ 推奨される実装
// 定数値の場合は通常のProvider
final constantDataProvider = Provider<String>((ref) {
return 'Constant Value';
});
// 検索結果など一時的なデータにはautoDisposeを使用
final searchResultProvider = FutureProvider.autoDispose<List<String>>((ref) async {
return await searchAPI();
});
5. family修飾子の誤った使用方法
❌ よくある間違い
// パラメータごとに新しいインスタンスが作られる
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
return await fetchUser(userId);
});
class UserWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 毎回新しいインスタンスを生成
final user = ref.watch(userProvider('123'));
return user.when(
data: (data) => Text(data.name),
loading: () => CircularProgressIndicator(),
error: (e, s) => Text('Error'),
);
}
}
✅ 推奨される実装
// パラメータを状態として管理
final selectedUserIdProvider = StateProvider<String?>((ref) => null);
final userProvider = FutureProvider<User?>((ref) async {
final userId = ref.watch(selectedUserIdProvider);
if (userId == null) return null;
return await fetchUser(userId);
});
class UserWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final user = ref.watch(userProvider);
return user.when(
data: (data) => data != null ? Text(data.name) : Text('Select user'),
loading: () => CircularProgressIndicator(),
error: (e, s) => Text('Error'),
);
}
}
6. エラーハンドリングの不適切な実装
❌ よくある間違い
final dataProvider = FutureProvider<Data>((ref) async {
try {
return await fetchData();
} catch (e) {
print(e); // エラーをログ出力のみ
return Data.empty(); // エラーを隠蔽
}
});
✅ 推奨される実装
final dataProvider = FutureProvider<Data>((ref) async {
try {
return await fetchData();
} catch (e, stackTrace) {
// エラーを適切に処理
ref.read(errorLoggerProvider).logError(e, stackTrace);
throw Exception('Failed to fetch data: $e');
}
});
class DataWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return data.when(
data: (data) => DataView(data),
loading: () => LoadingWidget(),
error: (error, stackTrace) => ErrorWidget(
error: error,
onRetry: () => ref.refresh(dataProvider),
),
);
}
}
まとめ
- ConsumerWidgetは必要な場所でのみ使用する
- selectを使用して必要な状態のみを監視する
- StateNotifierでの状態更新は適切に行う
- autoDisposeは必要な場合のみ使用する
- family修飾子は適切なユースケースで使用する
- エラーハンドリングは適切に実装する
これらのベストプラクティスを意識することで、より効率的で保守性の高いアプリケーションを開発することができます。
Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート3
1. UIとビジネスロジックの混在
❌ よくある間違い
final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
return CartNotifier();
});
class CartNotifier extends StateNotifier<List<CartItem>> {
CartNotifier() : super([]);
void addItem(CartItem item) {
state = [...state, item];
// UIに関する処理を含めてしまう
showToast('商品をカートに追加しました');
navigateToCart();
}
}
class CartScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
return Scaffold(
body: ListView.builder(
itemCount: cart.length,
itemBuilder: (context, index) {
final item = cart[index];
return ListTile(
title: Text(item.name),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () {
// Widgetで直接ビジネスロジックを実行
ref.read(cartProvider.notifier).removeItem(item);
calculateTotalPrice();
updateInventory();
},
),
);
},
),
);
}
}
✅ 推奨される実装
// ビジネスロジックを担当するService層
class CartService {
Future<void> addItemToCart(CartItem item) async {
await updateInventory(item);
await saveToDatabase(item);
return item;
}
Future<void> removeItemFromCart(CartItem item) async {
await updateInventory(item, isRemoval: true);
await removeFromDatabase(item);
}
}
// UIの状態管理を担当するProvider
final cartServiceProvider = Provider((ref) => CartService());
final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
return CartNotifier(ref.read(cartServiceProvider));
});
class CartNotifier extends StateNotifier<List<CartItem>> {
final CartService _cartService;
CartNotifier(this._cartService) : super([]);
Future<void> addItem(CartItem item) async {
await _cartService.addItemToCart(item);
state = [...state, item];
}
Future<void> removeItem(CartItem item) async {
await _cartService.removeItemFromCart(item);
state = state.where((i) => i.id != item.id).toList();
}
}
// UIレイヤー
class CartScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final cart = ref.watch(cartProvider);
return Scaffold(
body: ListView.builder(
itemCount: cart.length,
itemBuilder: (context, index) {
final item = cart[index];
return ListTile(
title: Text(item.name),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () async {
try {
await ref.read(cartProvider.notifier).removeItem(item);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('商品を削除しました')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラーが発生しました')),
);
}
},
),
);
},
),
);
}
}
2. 非同期状態の不適切な管理
❌ よくある間違い
final isLoadingProvider = StateProvider<bool>((ref) => false);
final errorProvider = StateProvider<String?>((ref) => null);
final dataProvider = StateProvider<List<Data>>((ref) => []);
class DataNotifier extends StateNotifier<List<Data>> {
DataNotifier() : super([]);
Future<void> fetchData() async {
ref.read(isLoadingProvider.notifier).state = true;
ref.read(errorProvider.notifier).state = null;
try {
final result = await api.fetchData();
ref.read(dataProvider.notifier).state = result;
} catch (e) {
ref.read(errorProvider.notifier).state = e.toString();
} finally {
ref.read(isLoadingProvider.notifier).state = false;
}
}
}
✅ 推奨される実装
// 非同期状態を表現するクラス
class AsyncState<T> {
final T? data;
final bool isLoading;
final String? error;
const AsyncState({
this.data,
this.isLoading = false,
this.error,
});
AsyncState<T> copyWith({
T? data,
bool? isLoading,
String? error,
}) {
return AsyncState<T>(
data: data ?? this.data,
isLoading: isLoading ?? this.isLoading,
error: error ?? this.error,
);
}
}
// 非同期状態を管理するProvider
final dataProvider = StateNotifierProvider<DataNotifier, AsyncState<List<Data>>>((ref) {
return DataNotifier();
});
class DataNotifier extends StateNotifier<AsyncState<List<Data>>> {
DataNotifier() : super(AsyncState());
Future<void> fetchData() async {
state = state.copyWith(isLoading: true, error: null);
try {
final result = await api.fetchData();
state = state.copyWith(
data: result,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}
}
// UI実装
class DataScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final asyncState = ref.watch(dataProvider);
if (asyncState.isLoading) {
return CircularProgressIndicator();
}
if (asyncState.error != null) {
return ErrorWidget(
message: asyncState.error!,
onRetry: () => ref.read(dataProvider.notifier).fetchData(),
);
}
final data = asyncState.data;
if (data == null || data.isEmpty) {
return EmptyStateWidget();
}
return ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => DataItem(data: data[index]),
);
}
}
3. キャッシュ制御の不適切な実装
❌ よくある間違い
// キャッシュ制御がない実装
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
return await api.fetchUser(userId);
});
✅ 推奨される実装
// キャッシュを制御するProvider
final userCacheProvider = StateProvider<Map<String, User>>((ref) => {});
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
// キャッシュをチェック
final cache = ref.watch(userCacheProvider);
if (cache.containsKey(userId)) {
return cache[userId]!;
}
// キャッシュがない場合はAPIから取得
final user = await api.fetchUser(userId);
// キャッシュを更新
ref.read(userCacheProvider.notifier).state = {
...cache,
userId: user,
};
return user;
});
// キャッシュを制御するメソッド
void clearUserCache(WidgetRef ref) {
ref.read(userCacheProvider.notifier).state = {};
}
void removeUserFromCache(WidgetRef ref, String userId) {
final cache = ref.read(userCacheProvider);
final newCache = Map<String, User>.from(cache)..remove(userId);
ref.read(userCacheProvider.notifier).state = newCache;
}
4. Providerの依存関係の循環参照
❌ よくある間違い
// 循環参照を引き起こす実装
final providerA = Provider((ref) {
final valueB = ref.watch(providerB);
return 'A: $valueB';
});
final providerB = Provider((ref) {
final valueA = ref.watch(providerA);
return 'B: $valueA';
});
✅ 推奨される実装
// 共通の状態を持つProviderを作成
final sharedStateProvider = StateProvider<String>((ref) => '');
// 依存関係を明確にした実装
final providerA = Provider((ref) {
final sharedState = ref.watch(sharedStateProvider);
return 'A: $sharedState';
});
final providerB = Provider((ref) {
final sharedState = ref.watch(sharedStateProvider);
return 'B: $sharedState';
});
// または、一方向の依存関係にする
final baseProvider = StateProvider<String>((ref) => 'base');
final derivedProviderA = Provider((ref) {
final base = ref.watch(baseProvider);
return 'A: $base';
});
final derivedProviderB = Provider((ref) {
final derivedA = ref.watch(derivedProviderA);
return 'B: $derivedA';
});
まとめ
-
UIとビジネスロジックは適切に分離する
- Service層を導入してビジネスロジックを分離
- UIの状態管理とビジネスロジックを明確に分ける
-
非同期状態は統合的に管理する
- 非同期状態を表現する専用のクラスを作成
- loading/error/dataの状態を一元管理
-
キャッシュは適切に制御する
- キャッシュの有効期限を設定
- メモリ使用量を考慮した実装
- キャッシュのクリア機能を提供
-
Provider間の依存関係は慎重に設計する
- 循環参照を避ける
- 依存関係を明確にする
- 共通の状態を適切に管理する
Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート4
1. リフレッシュ処理の不適切な実装
❌ よくある間違い
class HomeScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return RefreshIndicator(
onRefresh: () async {
// 直接Providerを再作成
ref.refresh(dataProvider);
// 他の関連Providerの更新を忘れている
},
child: ListView(
children: [
for (final item in data) ListTile(title: Text(item.title))
],
),
);
}
}
✅ 推奨される実装
// リフレッシュ用のProviderグループを作成
final refreshNotifierProvider = StateNotifierProvider<RefreshNotifier, void>((ref) {
return RefreshNotifier(ref);
});
class RefreshNotifier extends StateNotifier<void> {
final Ref ref;
RefreshNotifier(this.ref) : super(null);
Future<void> refresh() async {
// 関連する全てのProviderを更新
await Future.wait([
ref.refresh(dataProvider.future),
ref.refresh(userProvider.future),
ref.refresh(settingsProvider.future),
]);
// キャッシュのクリア
ref.read(cacheProvider.notifier).clear();
// 必要に応じて追加の処理
await ref.read(analyticsProvider).logRefresh();
}
}
class HomeScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(dataProvider);
return RefreshIndicator(
onRefresh: () => ref.read(refreshNotifierProvider.notifier).refresh(),
child: data.when(
data: (items) => ListView(
children: [
for (final item in items) ListTile(title: Text(item.title))
],
),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error: error),
),
);
}
}
2. ページネーションの実装ミス
❌ よくある間違い
final pagedDataProvider = StateNotifierProvider<PagedDataNotifier, List<Item>>((ref) {
return PagedDataNotifier();
});
class PagedDataNotifier extends StateNotifier<List<Item>> {
int _page = 1;
bool _isLoading = false;
PagedDataNotifier() : super([]);
Future<void> loadMore() async {
if (_isLoading) return;
_isLoading = true;
try {
final newItems = await api.fetchItems(page: _page);
state = [...state, ...newItems];
_page++;
} finally {
_isLoading = false;
}
}
}
✅ 推奨される実装
// ページネーションの状態を管理するクラス
class PaginationState<T> {
final List<T> items;
final bool isLoading;
final String? error;
final bool hasMoreItems;
final int currentPage;
PaginationState({
required this.items,
this.isLoading = false,
this.error,
this.hasMoreItems = true,
this.currentPage = 1,
});
PaginationState<T> copyWith({
List<T>? items,
bool? isLoading,
String? error,
bool? hasMoreItems,
int? currentPage,
}) {
return PaginationState<T>(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
error: error,
hasMoreItems: hasMoreItems ?? this.hasMoreItems,
currentPage: currentPage ?? this.currentPage,
);
}
}
final pagedDataProvider = StateNotifierProvider<PagedDataNotifier, PaginationState<Item>>((ref) {
return PagedDataNotifier();
});
class PagedDataNotifier extends StateNotifier<PaginationState<Item>> {
static const int _pageSize = 20;
PagedDataNotifier() : super(PaginationState(items: []));
Future<void> loadMore() async {
if (state.isLoading || !state.hasMoreItems) return;
state = state.copyWith(isLoading: true, error: null);
try {
final newItems = await api.fetchItems(
page: state.currentPage,
pageSize: _pageSize,
);
final hasMore = newItems.length == _pageSize;
state = state.copyWith(
items: [...state.items, ...newItems],
currentPage: state.currentPage + 1,
hasMoreItems: hasMore,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
error: e.toString(),
isLoading: false,
);
}
}
Future<void> refresh() async {
state = PaginationState(items: []);
await loadMore();
}
}
// UI実装
class PagedListView extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final state = ref.watch(pagedDataProvider);
return RefreshIndicator(
onRefresh: () => ref.read(pagedDataProvider.notifier).refresh(),
child: ListView.builder(
itemCount: state.items.length + (state.hasMoreItems ? 1 : 0),
itemBuilder: (context, index) {
if (index == state.items.length) {
if (state.error != null) {
return ErrorItem(
error: state.error!,
onRetry: () => ref.read(pagedDataProvider.notifier).loadMore(),
);
}
ref.read(pagedDataProvider.notifier).loadMore();
return LoadingItem();
}
return ItemWidget(item: state.items[index]);
},
),
);
}
}
3. 状態の永続化の不適切な実装
❌ よくある間違い
final persistentDataProvider = StateNotifierProvider<PersistentDataNotifier, Map<String, dynamic>>((ref) {
return PersistentDataNotifier();
});
class PersistentDataNotifier extends StateNotifier<Map<String, dynamic>> {
PersistentDataNotifier() : super({}) {
_loadData();
}
Future<void> _loadData() async {
final prefs = await SharedPreferences.getInstance();
final data = prefs.getString('data');
if (data != null) {
state = json.decode(data);
}
}
Future<void> saveData(String key, dynamic value) async {
final prefs = await SharedPreferences.getInstance();
state = {...state, key: value};
await prefs.setString('data', json.encode(state));
}
}
✅ 推奨される実装
// 永続化するデータのモデル
class PersistentData with _$PersistentData {
const factory PersistentData({
required Map<String, dynamic> data,
required DateTime lastUpdated,
String? error,
}) = _PersistentData;
factory PersistentData.fromJson(Map<String, dynamic> json) =>
_$PersistentDataFromJson(json);
}
// 永続化を担当するService
class StorageService {
final SharedPreferences _prefs;
StorageService(this._prefs);
static Future<StorageService> init() async {
final prefs = await SharedPreferences.getInstance();
return StorageService(prefs);
}
Future<void> saveData(String key, String value) async {
await _prefs.setString(key, value);
}
String? getData(String key) {
return _prefs.getString(key);
}
Future<void> removeData(String key) async {
await _prefs.remove(key);
}
}
// StorageServiceのProvider
final storageServiceProvider = Provider<StorageService>((ref) {
throw UnimplementedError();
});
// 永続化データのProvider
final persistentDataProvider = StateNotifierProvider<PersistentDataNotifier, AsyncValue<PersistentData>>((ref) {
return PersistentDataNotifier(ref.watch(storageServiceProvider));
});
class PersistentDataNotifier extends StateNotifier<AsyncValue<PersistentData>> {
final StorageService _storage;
static const String _storageKey = 'persistent_data';
PersistentDataNotifier(this._storage) : super(const AsyncValue.loading()) {
_loadData();
}
Future<void> _loadData() async {
try {
final jsonStr = _storage.getData(_storageKey);
if (jsonStr != null) {
final data = PersistentData.fromJson(json.decode(jsonStr));
state = AsyncValue.data(data);
} else {
state = AsyncValue.data(PersistentData(
data: {},
lastUpdated: DateTime.now(),
));
}
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
Future<void> updateData(String key, dynamic value) async {
state.whenData((currentData) async {
try {
final newData = PersistentData(
data: {...currentData.data, key: value},
lastUpdated: DateTime.now(),
);
await _storage.saveData(
_storageKey,
json.encode(newData.toJson()),
);
state = AsyncValue.data(newData);
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
});
}
Future<void> clear() async {
try {
await _storage.removeData(_storageKey);
state = AsyncValue.data(PersistentData(
data: {},
lastUpdated: DateTime.now(),
));
} catch (e, stack) {
state = AsyncValue.error(e, stack);
}
}
}
// アプリケーションの初期化
Future<void> initializeApp() async {
final storage = await StorageService.init();
runApp(
ProviderScope(
overrides: [
storageServiceProvider.overrideWithValue(storage),
],
child: MyApp(),
),
);
}
まとめ
-
リフレッシュ処理は包括的に実装する
- 関連する全てのProviderを更新
- キャッシュの制御も忘れずに
- エラーハンドリングを適切に実装
-
ページネーションは状態を明確に管理する
- ローディング状態、エラー状態、データの有無を管理
- 重複読み込みを防ぐ
- リフレッシュ機能も実装
-
永続化は堅牢に実装する
- データの型を明確に定義
- エラーハンドリングを適切に実装
- 非同期処理を適切に管理
- 依存関係の注入を適切に行う
Riverpodで初学者がよく陥る実装の落とし穴と解決方法 パート5
1. Providerの状態監視における共通のミス
❌ 不適切なwatch/readの使用例
// 例1: buildメソッド内でのread使用
class ProductWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 誤り: buildメソッド内でreadを使用
final product = ref.read(productProvider);
return Text(product.name); // 更新が反映されない
}
}
// 例2: イベントハンドラ内でのwatch使用
class CartButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// 誤り: イベントハンドラ内でwatchを使用
final cart = ref.watch(cartProvider);
cart.addItem(product);
},
child: Text('カートに追加'),
);
}
}
// 例3: 必要以上の監視
class PriceWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 誤り: 製品全体を監視している
final product = ref.watch(productProvider);
// 価格のみを表示
return Text('¥${product.price}');
}
}
✅ 適切な実装例
// 例1: buildメソッドでの適切なwatch使用
class ProductWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 正しい: buildメソッド内でwatchを使用
final product = ref.watch(productProvider);
return Text(product.name); // 更新が反映される
}
}
// 例2: イベントハンドラでの適切なread使用
class CartButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () {
// 正しい: イベントハンドラ内でreadを使用
ref.read(cartProvider.notifier).addItem(product);
},
child: Text('カートに追加'),
);
}
}
// 例3: 必要な値のみの監視
class PriceWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 正しい: 価格のみを監視
final price = ref.watch(productProvider.select((product) => product.price));
return Text('¥$price');
}
}
// 例4: 複雑な状態変更を伴うケース
class ComplexProductWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
// 複数の状態を監視
final product = ref.watch(productProvider);
final isInCart = ref.watch(cartProvider.select(
(cart) => cart.items.any((item) => item.id == product.id)
));
final isFavorite = ref.watch(favoriteProvider.select(
(favorites) => favorites.contains(product.id)
));
return Card(
child: Column(
children: [
Text(product.name),
Text('¥${product.price}'),
Row(
children: [
IconButton(
icon: Icon(
isFavorite ? Icons.favorite : Icons.favorite_border,
color: isFavorite ? Colors.red : null,
),
onPressed: () {
// 状態更新はnotifierを通じて行う
ref.read(favoriteProvider.notifier).toggleFavorite(product.id);
},
),
ElevatedButton(
onPressed: isInCart
? null
: () => ref.read(cartProvider.notifier).addItem(product),
child: Text(isInCart ? 'カート追加済み' : 'カートに追加'),
),
],
),
],
),
);
}
}
2. 非同期データの詳細な制御
❌ 不適切な非同期処理の実装
// 例1: ローディング状態の不適切な管理
final productsProvider = FutureProvider((ref) async {
final products = await fetchProducts();
return products;
});
// 例2: エラーハンドリングの欠如
final searchProvider = FutureProvider.family<List<Product>, String>((ref, query) async {
return await api.searchProducts(query);
});
// 例3: キャッシュ制御の欠如
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
return await api.fetchUser(userId);
});
✅ 適切な実装例
// 例1: 詳細な非同期状態管理
final productsProvider = FutureProvider.autoDispose((ref) async {
// キャンセルトークンの使用
final cancelToken = CancellationToken();
ref.onDispose(() => cancelToken.cancel());
// デバウンス処理
await Future.delayed(const Duration(milliseconds: 300));
try {
final products = await fetchProducts(cancelToken: cancelToken);
// 結果が空の場合の処理
if (products.isEmpty) {
throw const NoProductsFoundException();
}
return products;
} on DioError catch (e) {
// ネットワークエラーの変換
throw NetworkException(e.message);
} catch (e) {
throw UnexpectedError(e.toString());
}
});
// 例2: 高度な検索機能の実装
final searchProvider = StateNotifierProvider.autoDispose<SearchNotifier, AsyncValue<List<Product>>>((ref) {
return SearchNotifier(ref);
});
class SearchNotifier extends StateNotifier<AsyncValue<List<Product>>> {
final AutoDisposeStateNotifierProviderRef _ref;
Timer? _debounceTimer;
SearchNotifier(this._ref) : super(const AsyncValue.loading()) {
_ref.onDispose(() {
_debounceTimer?.cancel();
});
}
Future<void> search(String query) async {
_debounceTimer?.cancel();
if (query.isEmpty) {
state = const AsyncValue.data([]);
return;
}
state = const AsyncValue.loading();
_debounceTimer = Timer(const Duration(milliseconds: 500), () async {
try {
final results = await api.searchProducts(query);
if (!mounted) return;
state = AsyncValue.data(results);
} catch (e, stack) {
if (!mounted) return;
state = AsyncValue.error(e, stack);
}
});
}
}
// 例3: キャッシュを含む高度なユーザー情報管理
final userCacheProvider = StateProvider<Map<String, User>>((ref) => {});
final userProvider = FutureProvider.family<User, String>((ref, userId) async {
final cache = ref.watch(userCacheProvider);
// キャッシュチェック
if (cache.containsKey(userId)) {
final cachedUser = cache[userId]!;
final cacheAge = DateTime.now().difference(cachedUser.lastFetched);
// キャッシュが新しい場合はそれを使用
if (cacheAge < const Duration(minutes: 5)) {
return cachedUser;
}
}
// 新しいデータをフェッチ
final user = await api.fetchUser(userId);
// キャッシュを更新
ref.read(userCacheProvider.notifier).state = {
...cache,
userId: user.copyWith(lastFetched: DateTime.now()),
};
return user;
});
// 使用例
class UserProfileWidget extends ConsumerWidget {
final String userId;
const UserProfileWidget({required this.userId});
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider(userId));
return userAsync.when(
data: (user) => Column(
children: [
Text(user.name),
Text(user.email),
ElevatedButton(
onPressed: () {
// キャッシュを強制的にリフレッシュ
ref.refresh(userProvider(userId));
},
child: Text('更新'),
),
],
),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(
error: error,
onRetry: () => ref.refresh(userProvider(userId)),
),
);
}
}
// エラーウィジェット
class ErrorWidget extends StatelessWidget {
final Object error;
final VoidCallback onRetry;
const ErrorWidget({
required this.error,
required this.onRetry,
});
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
error is NetworkException
? 'ネットワークエラーが発生しました'
: error is NoProductsFoundException
? '商品が見つかりませんでした'
: 'エラーが発生しました',
style: TextStyle(color: Colors.red),
),
ElevatedButton(
onPressed: onRetry,
child: Text('再試行'),
),
],
);
}
}
3. Providerの依存関係の適切な管理
❌ 不適切な依存関係の管理
// 例1: 密結合な依存関係
final cartProvider = StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
final products = ref.watch(productsProvider);
final user = ref.watch(userProvider);
return CartNotifier(products, user); // 直接依存
});
// 例2: グローバルな状態アクセス
class ProductService {
void updateProduct(Product product) {
// グローバル変数としてProviderContainerにアクセス
container.read(productsProvider.notifier).updateProduct(product);
}
}
✅ 適切な実装例
// 例1: 依存性の注入を使用した実装
abstract class ProductRepository {
Future<List<Product>> getProducts();
Future<void> updateProduct(Product product);
}
class ApiProductRepository implements ProductRepository {
final Dio _dio;
ApiProductRepository(this._dio);
Future<List<Product>> getProducts() async {
final response = await _dio.get('/products');
return (response.data as List)
.map((json) => Product.fromJson(json))
.toList();
}
Future<void> updateProduct(Product product) async {
await _dio.put('/products/${product.id}', data: product.toJson());
}
}
final productRepositoryProvider = Provider<ProductRepository>((ref) {
final dio = ref.watch(dioProvider);
return ApiProductRepository(dio);
});
// 例2: 疎結合な依存関係
final productServiceProvider = Provider<ProductService>((ref) {
final repository = ref.watch(productRepositoryProvider);
return ProductService(repository);
});
class ProductService {
final ProductRepository _repository;
ProductService(this._repository);
Future<void> updateProduct(Product product) async {
await _repository.updateProduct(product);
}
}
// 例3: 複雑な依存関係の管理
final cartServiceProvider = Provider<CartService>((ref) {
final productRepo = ref.watch(productRepositoryProvider);
final userRepo = ref.watch(userRepositoryProvider);
final analytics = ref.watch(analyticsServiceProvider);
return CartService(
productRepository: productRepo,
userRepository: userRepo,
analytics: analytics,
);
});
class CartService {
final ProductRepository _productRepository;
final UserRepository _userRepository;
final AnalyticsService _analytics;
CartService({
required ProductRepository productRepository,
required UserRepository userRepository,
required AnalyticsService analytics,
}) : _productRepository = productRepository,
_userRepository = userRepository,
_analytics = analytics;
Future<void> addToCart(String productId) async {
try {
final product = await _productRepository.getProduct(productId);
final user = await _userRepository.getCurrentUser();
await _productRepository.updateStock(productId, -1);
await _userRepository.addToCart(user.id, product);
_analytics.logAddToCart(product);
} catch (e) {
// エラーハンドリング
rethrow;
}
}
}
// 使用例
class AddToCartButton extends ConsumerWidget {
final String productId;
const AddToCartButton({required this.productId});
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton(
onPressed: () async {
try {
await ref.read(cartServiceProvider).addToCart(productId);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('カートに追加しました')),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('エラーが発生しました')),
);
}
},
child: Text('カートに追加'),
);
}
}
まとめ
-
状態監視の基本ルール
- buildメソッド内ではwatchを使用
- イベントハンドラ内ではreadを使用
- 必要な値のみをselectで監視
- 状態更新は必ずnotifierを通じて行う
-
非同期データの扱い方
- 適切なローディング状態の管理
- エラーハンドリングの実装
- キャンセル処理の実装
- キャッシュ制御の実装
- デバウンス処理の実装
-
依存関係の管理
- 依存性の注入を活用
- インターフェースを使用した疎結合な設計
- 適切な責任分離
Riverpodで初学者がよく悩む実装パターンと解決方法
1. 基本的なPODOの実装パターン
❌ よくある実装ミス
// 例1: 不完全なモデルクラス
class User {
final String name;
final String email;
User(this.name, this.email); // コピーメソッドがない
}
// 例2: バリデーションの欠如
class Product {
final String name;
final int price;
Product(this.name, this.price); // 値の検証がない
}
// 例3: 不適切なJSONシリアライズ
class Order {
final String id;
final DateTime createdAt;
Order(this.id, this.createdAt);
Map<String, dynamic> toJson() {
return {
'id': id,
'createdAt': createdAt.toString(), // 不適切な日付形式
};
}
}
✅ 推奨される実装
// 例1: 完全なモデルクラス
class User with _$User {
const factory User({
required String id,
required String name,
required String email,
required DateTime createdAt,
String? photoUrl,
}) = _User;
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
}
// 例2: バリデーションを含むモデル
class Product with _$Product {
const factory Product({
required String id,
required String name,
required int price,
required int stock,
(false) bool isPublished,
}) = _Product;
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
// カスタムバリデーションメソッド
static Product? validate({
required String name,
required int price,
required int stock,
}) {
if (name.isEmpty) return null;
if (price < 0) return null;
if (stock < 0) return null;
return Product(
id: const Uuid().v4(),
name: name,
price: price,
stock: stock,
);
}
}
// 例3: 適切なJSONシリアライズ
class Order with _$Order {
const factory Order({
required String id,
required List<OrderItem> items,
required DateTime createdAt,
required OrderStatus status,
String? couponCode,
}) = _Order;
factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
// カスタムゲッター
int get totalAmount => items.fold(
0,
(sum, item) => sum + (item.price * item.quantity),
);
bool get canCancel => status == OrderStatus.pending;
}
// 列挙型の定義
enum OrderStatus {
pending,
processing,
shipped,
delivered,
cancelled;
String get displayName {
switch (this) {
case OrderStatus.pending:
return '注文受付';
case OrderStatus.processing:
return '処理中';
case OrderStatus.shipped:
return '発送済み';
case OrderStatus.delivered:
return '配達完了';
case OrderStatus.cancelled:
return 'キャンセル';
}
}
}
2. Provider定義のベストプラクティス
❌ よくある実装ミス
// 例1: グローバル変数としてのProvider定義
final userProvider = StateNotifierProvider<UserNotifier, User>((ref) => UserNotifier());
// 例2: 過度に複雑なProvider
final complexProvider = StateNotifierProvider<ComplexNotifier, ComplexState>((ref) {
final service = ref.watch(serviceProvider);
final repository = ref.watch(repositoryProvider);
final analytics = ref.watch(analyticsProvider);
final cache = ref.watch(cacheProvider);
return ComplexNotifier(service, repository, analytics, cache);
});
// 例3: 不適切なProviderの種類選択
final counterProvider = Provider((ref) {
return 0; // 変更可能な値なのにProviderを使用
});
✅ 推奨される実装
// 例1: 適切なProvider定義
// Providerの定義をクラスとしてまとめる
class Providers {
// プライベートコンストラクタでインスタンス化を防ぐ
Providers._();
// ユーザー関連のProvider
static final userProvider = StateNotifierProvider<UserNotifier, AsyncValue<User?>>((ref) {
final repository = ref.watch(repositoryProvider);
final analytics = ref.watch(analyticsProvider);
return UserNotifier(repository, analytics);
});
// 認証状態のProvider
static final authStateProvider = StreamProvider<AuthState>((ref) {
return ref.watch(firebaseAuthProvider).authStateChanges().map((user) {
if (user == null) return const AuthState.unauthenticated();
return AuthState.authenticated(user);
});
});
// ユーザー設定のProvider
static final userSettingsProvider = StateNotifierProvider<UserSettingsNotifier, UserSettings>((ref) {
// 認証状態を監視して設定を更新
ref.listen<AuthState>(authStateProvider, (previous, next) {
next.whenOrNull(
unauthenticated: () => ref.invalidate(userSettingsProvider),
);
});
return UserSettingsNotifier(ref.watch(settingsRepositoryProvider));
});
}
// 例2: 適切な粒度のProvider
class UserNotifier extends StateNotifier<AsyncValue<User?>> {
final UserRepository _repository;
final AnalyticsService _analytics;
UserNotifier(this._repository, this._analytics)
: super(const AsyncValue.loading()) {
_init();
}
Future<void> _init() async {
state = const AsyncValue.loading();
try {
final user = await _repository.getCurrentUser();
state = AsyncValue.data(user);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
Future<void> updateProfile({
String? name,
String? photoUrl,
}) async {
final currentUser = state.valueOrNull;
if (currentUser == null) return;
state = const AsyncValue.loading();
try {
final updatedUser = await _repository.updateUser(
currentUser.id,
name: name,
photoUrl: photoUrl,
);
_analytics.logProfileUpdate();
state = AsyncValue.data(updatedUser);
} catch (error, stackTrace) {
state = AsyncValue.error(error, stackTrace);
}
}
}
// 例3: 適切なProviderの種類選択
// 単純な値の場合
final counterProvider = StateProvider<int>((ref) => 0);
// 複雑な状態管理の場合
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
return CartNotifier(ref.watch(cartRepositoryProvider));
});
// 非同期データの場合
final productsProvider = FutureProvider.autoDispose((ref) async {
ref.keepAlive(); // 必要に応じてキャッシュを維持
return ref.watch(productRepositoryProvider).getProducts();
});
// ストリームデータの場合
final messagesProvider = StreamProvider.autoDispose<List<Message>>((ref) {
final chatId = ref.watch(currentChatIdProvider);
return ref.watch(messageRepositoryProvider).watchMessages(chatId);
});
3. Provider間の依存関係の管理
❌ よくある実装ミス
// 例1: 直接的な依存関係
final dataProvider = Provider((ref) {
final service = ServiceImpl(); // 直接インスタンス化
return service.getData();
});
// 例2: 循環依存
final providerA = Provider((ref) {
final b = ref.watch(providerB);
return 'A: $b';
});
final providerB = Provider((ref) {
final a = ref.watch(providerA); // 循環依存
return 'B: $a';
});
// 例3: 不適切な状態共有
final globalStateProvider = StateProvider((ref) => 0);
✅ 推奨される実装
// 例1: レイヤー分けされた依存関係
// 1. 設定Provider
final configProvider = Provider((ref) {
return AppConfig(
apiUrl: 'https://api.example.com',
timeout: const Duration(seconds: 30),
);
});
// 2. HTTP ClientのProvider
final dioProvider = Provider((ref) {
final config = ref.watch(configProvider);
final dio = Dio()
..options.baseUrl = config.apiUrl
..options.connectTimeout = config.timeout
..options.receiveTimeout = config.timeout
..interceptors.addAll([
LogInterceptor(),
AuthInterceptor(),
]);
ref.onDispose(() {
dio.close();
});
return dio;
});
// 3. RepositoryのProvider
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepositoryImpl(ref.watch(dioProvider));
});
// 4. ServiceのProvider
final userServiceProvider = Provider<UserService>((ref) {
return UserServiceImpl(ref.watch(userRepositoryProvider));
});
// 5. 状態管理のProvider
final userStateProvider = StateNotifierProvider<UserStateNotifier, AsyncValue<User?>>((ref) {
return UserStateNotifier(ref.watch(userServiceProvider));
});
// 例2: 適切な状態分離
// 認証状態
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(ref.watch(authRepositoryProvider));
});
// ユーザープロフィール
final profileProvider = FutureProvider<Profile?>((ref) {
final authState = ref.watch(authProvider);
return authState.maybeWhen(
authenticated: (user) => ref.watch(profileRepositoryProvider).getProfile(user.id),
orElse: () => null,
);
});
// 設定
final settingsProvider = StateNotifierProvider<SettingsNotifier, Settings>((ref) {
ref.listen<AuthState>(authProvider, (previous, next) {
// 認証状態が変更されたら設定をリセット
next.maybeWhen(
unauthenticated: () => ref.invalidate(settingsProvider),
orElse: () {},
);
});
return SettingsNotifier(ref.watch(settingsRepositoryProvider));
});
// 例3: 機能ごとの状態管理
class CartState {
final List<CartItem> items;
final bool isLoading;
final String? error;
const CartState({
required this.items,
this.isLoading = false,
this.error,
});
CartState copyWith({
List<CartItem>? items,
bool? isLoading,
String? error,
}) {
return CartState(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
error: error,
);
}
int get totalQuantity => items.fold(0, (sum, item) => sum + item.quantity);
int get totalAmount => items.fold(0, (sum, item) => sum + item.totalPrice);
}
final cartProvider = StateNotifierProvider<CartNotifier, CartState>((ref) {
// 依存するProviderを注入
final repository = ref.watch(cartRepositoryProvider);
final analytics = ref.watch(analyticsProvider);
// 認証状態の変更を監視
ref.listen<AuthState>(authProvider, (previous, next) {
next.maybeWhen(
unauthenticated: () => ref.invalidate(cartProvider),
orElse: () {},
);
});
return CartNotifier(repository, analytics);
});
class CartNotifier extends StateNotifier<CartState> {
final CartRepository _repository;
final AnalyticsService _analytics;
CartNotifier(this._repository, this._analytics)
: super(const CartState(items: [])) {
_loadCart();
}
Future<void> _loadCart() async {
state = state.copyWith(isLoading: true, error: null);
try {
final items = await _repository.getCartItems();
state = state.copyWith(items: items, isLoading: false);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: 'カートの読み込みに失敗しました',
);
}
}
Future<void> addItem(Product product, [int quantity = 1]) async {
state = state.copyWith(isLoading: true, error: null);
try {
await _repository.addToCart(product.id, quantity);
final items = await _repository.getCartItems();
_analytics.logAddToCart(product, quantity);
state = state.copyWith(items: items, isLoading: false);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: '商品の追加に失敗しました',
);
}
}
}
まとめのポイント
-
モデルクラスの実装
- freezedを活用した不変オブジェクトの作成
- 適切なバリデーション
- JSONシリアライズ/デシリアライズの実装
- カスタムメソッドやゲッターの活用
-
Provider定義のベストプラクティス
- 適切なスコープでの定義
- 責任の分離
- 適切なProvider種類の選択
- エラーハンドリング
- 依存関係の注入
-
Provider間の依存関係
- レイヤー分けによる依存関係の整理
- 適切な状態分離
- 機能ごとの状態管理
- リスナーを使用した状態の連携
これらの実装パターンを意識することで、より保守性が高く、理解しやすいコードを書くことができます。と
Riverpodでよくある間違いと正しい実装 - より実践的なパターン
1. ログイン状態の管理
❌ 間違い1: 単純なログイン状態管理
final isLoggedInProvider = StateProvider<bool>((ref) => false);
class LoginButton extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final isLoggedIn = ref.watch(isLoggedInProvider);
return ElevatedButton(
onPressed: () async {
await FirebaseAuth.instance.signInWithEmailAndPassword(...);
ref.read(isLoggedInProvider.notifier).state = true; // 単純なbool値の管理
},
child: Text(isLoggedIn ? 'ログアウト' : 'ログイン'),
);
}
}
✅ 正解1: 包括的な認証状態管理
// 認証状態を表現する
sealed class AuthState {
const AuthState();
const factory AuthState.initial() = _Initial;
const factory AuthState.authenticated(User user) = _Authenticated;
const factory AuthState.unauthenticated() = _Unauthenticated;
const factory AuthState.loading() = _Loading;
const factory AuthState.error(String message) = _Error;
}
final authProvider = StateNotifierProvider<AuthNotifier, AuthState>((ref) {
return AuthNotifier(ref.watch(authRepositoryProvider));
});
class AuthNotifier extends StateNotifier<AuthState> {
final AuthRepository _repository;
StreamSubscription? _authStateSubscription;
AuthNotifier(this._repository) : super(const AuthState.initial()) {
_initialize();
}
void _initialize() {
_authStateSubscription = _repository.authStateChanges().listen(
(user) {
if (user != null) {
state = AuthState.authenticated(user);
} else {
state = const AuthState.unauthenticated();
}
},
onError: (error) {
state = AuthState.error(error.toString());
},
);
}
Future<void> signIn(String email, String password) async {
state = const AuthState.loading();
try {
await _repository.signIn(email, password);
// 状態の更新は_initializeのリスナーで行われる
} catch (e) {
state = AuthState.error('ログインに失敗しました: ${e.toString()}');
}
}
void dispose() {
_authStateSubscription?.cancel();
super.dispose();
}
}
❌ 間違い2: 認証後のユーザー情報取得
final userProvider = FutureProvider<User?>((ref) async {
final auth = FirebaseAuth.instance;
return auth.currentUser; // 単純なユーザー情報の取得
});
✅ 正解2: 詳細なユーザー情報管理
final userProvider = FutureProvider.family<UserProfile, String>((ref, userId) async {
// 認証状態を監視
final authState = ref.watch(authProvider);
return authState.maybeWhen(
authenticated: (authUser) async {
// キャッシュをチェック
final cached = ref.read(userCacheProvider)[userId];
if (cached != null && cached.isValid) {
return cached.data;
}
final userProfile = await ref.read(userRepositoryProvider).getUserProfile(userId);
// キャッシュを更新
ref.read(userCacheProvider.notifier).cacheUser(userId, userProfile);
// 追加情報の取得
ref.read(userPreferencesProvider.notifier).loadPreferences(userId);
return userProfile;
},
orElse: () => throw UnauthorizedException(),
);
});
// キャッシュ管理
final userCacheProvider = StateNotifierProvider<UserCacheNotifier, Map<String, CachedData<UserProfile>>>((ref) {
return UserCacheNotifier();
});
class CachedData<T> {
final T data;
final DateTime cachedAt;
final Duration validity;
CachedData({
required this.data,
required this.cachedAt,
this.validity = const Duration(minutes: 5),
});
bool get isValid =>
DateTime.now().difference(cachedAt) < validity;
}
2. フォーム状態の管理
❌ 間違い3: 個別のフォームフィールド管理
final emailProvider = StateProvider<String>((ref) => '');
final passwordProvider = StateProvider<String>((ref) => '');
final isLoadingProvider = StateProvider<bool>((ref) => false);
class LoginForm extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final email = ref.watch(emailProvider);
final password = ref.watch(passwordProvider);
final isLoading = ref.watch(isLoadingProvider);
return Column(
children: [
TextField(
onChanged: (value) => ref.read(emailProvider.notifier).state = value,
),
TextField(
onChanged: (value) => ref.read(passwordProvider.notifier).state = value,
),
ElevatedButton(
onPressed: isLoading ? null : () async {
ref.read(isLoadingProvider.notifier).state = true;
// ログイン処理
ref.read(isLoadingProvider.notifier).state = false;
},
child: Text('ログイン'),
),
],
);
}
}
✅ 正解3: 統合的なフォーム状態管理
// フォームの状態を定義
class LoginFormState with _$LoginFormState {
const factory LoginFormState({
('') String email,
('') String password,
(false) bool isSubmitting,
(false) bool isValid,
String? errorMessage,
}) = _LoginFormState;
}
final loginFormProvider = StateNotifierProvider<LoginFormNotifier, LoginFormState>((ref) {
return LoginFormNotifier(ref.watch(authRepositoryProvider));
});
class LoginFormNotifier extends StateNotifier<LoginFormState> {
final AuthRepository _authRepository;
LoginFormNotifier(this._authRepository) : super(const LoginFormState());
void emailChanged(String email) {
state = state.copyWith(
email: email,
isValid: _validateForm(email: email, password: state.password),
errorMessage: null,
);
}
void passwordChanged(String password) {
state = state.copyWith(
password: password,
isValid: _validateForm(email: state.email, password: password),
errorMessage: null,
);
}
bool _validateForm({required String email, required String password}) {
if (email.isEmpty || !email.contains('@')) return false;
if (password.isEmpty || password.length < 6) return false;
return true;
}
Future<void> submit() async {
if (!state.isValid || state.isSubmitting) return;
state = state.copyWith(isSubmitting: true, errorMessage: null);
try {
await _authRepository.signIn(
email: state.email,
password: state.password,
);
} catch (e) {
state = state.copyWith(
isSubmitting: false,
errorMessage: e.toString(),
);
return;
}
state = state.copyWith(isSubmitting: false);
}
}
// UI実装
class LoginForm extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final formState = ref.watch(loginFormProvider);
return Form(
child: Column(
children: [
TextFormField(
decoration: InputDecoration(
labelText: 'メールアドレス',
errorText: !formState.email.contains('@') && formState.email.isNotEmpty
? '有効なメールアドレスを入力してください'
: null,
),
onChanged: ref.read(loginFormProvider.notifier).emailChanged,
),
TextFormField(
decoration: InputDecoration(
labelText: 'パスワード',
errorText: formState.password.length < 6 && formState.password.isNotEmpty
? 'パスワードは6文字以上必要です'
: null,
),
obscureText: true,
onChanged: ref.read(loginFormProvider.notifier).passwordChanged,
),
if (formState.errorMessage != null)
Text(
formState.errorMessage!,
style: TextStyle(color: Colors.red),
),
ElevatedButton(
onPressed: formState.isValid && !formState.isSubmitting
? () => ref.read(loginFormProvider.notifier).submit()
: null,
child: formState.isSubmitting
? CircularProgressIndicator()
: Text('ログイン'),
),
],
),
);
}
}
3. API通信の状態管理
❌ 間違い4: 単純なAPI通信
final productsProvider = FutureProvider<List<Product>>((ref) async {
final response = await http.get('https://api.example.com/products');
return (jsonDecode(response.body) as List)
.map((json) => Product.fromJson(json))
.toList();
});
✅ 正解4: エラーハンドリングとキャッシュを含むAPI通信
// APIの状態を表現
class ApiState<T> with _$ApiState<T> {
const factory ApiState.initial() = _Initial;
const factory ApiState.loading() = _Loading;
const factory ApiState.success(T data) = _Success;
const factory ApiState.error(String message) = _Error;
}
final productsProvider = StateNotifierProvider<ProductsNotifier, ApiState<List<Product>>>((ref) {
return ProductsNotifier(ref.watch(productRepositoryProvider));
});
class ProductsNotifier extends StateNotifier<ApiState<List<Product>>> {
final ProductRepository _repository;
Timer? _debounceTimer;
ProductsNotifier(this._repository) : super(const ApiState.initial()) {
fetchProducts();
}
Future<void> fetchProducts({String? search}) async {
// 既存のデバウンスタイマーをキャンセル
_debounceTimer?.cancel();
// 検索クエリがある場合はデバウンス
if (search != null) {
_debounceTimer = Timer(const Duration(milliseconds: 300), () {
_fetchProducts(search: search);
});
return;
}
await _fetchProducts(search: search);
}
Future<void> _fetchProducts({String? search}) async {
state = const ApiState.loading();
try {
final products = await _repository.getProducts(search: search);
if (!mounted) return;
// キャッシュの更新
ref.read(productCacheProvider.notifier).updateProducts(products);
state = ApiState.success(products);
} on NetworkException catch (e) {
state = ApiState.error('ネットワークエラー: ${e.message}');
} on ApiException catch (e) {
state = ApiState.error('APIエラー: ${e.message}');
} catch (e) {
state = ApiState.error('予期せぬエラーが発生しました');
}
}
void dispose() {
_debounceTimer?.cancel();
super.dispose();
}
}
// UI実装
class ProductsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final productsState = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: Text('商品一覧'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => ref.refresh(productsProvider),
),
],
),
body: productsState.when(
initial: () => const SizedBox(),
loading: () => const Center(child: CircularProgressIndicator()),
success: (products) => products.isEmpty
? const Center(child: Text('商品がありません'))
: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) => ProductCard(
product: products[index],
onTap: () => _navigateToDetail(context, products[index]),
),
),
error: (message) => ErrorView(
message: message,
onRetry: () => ref.refresh(productsProvider),
),
),
);
}
}
これらの実装例は、より実践的な状況での正しい実装パターンを示しています。要点は:
-
状態管理
- 単純な値ではなく、詳細な状態を表現する
- エラーハンドリングを適切に行う
- ローディング状態を管理する
-
キャッシュ管理
- データの有効期限を設定
- メモリ使用量を考慮
- 適切なタイミングでの更新
-
ユーザー体験
- ローディング表示
- エラーメッセージの表示
- リトライ機能の提供
これらのパターンを意識することで、より堅牢なアプリケーションを開発することができます。
Riverpodの実践的な実装パターン
1. 検索機能の実装
❌ 間違い: 単純な検索実装
// 検索キーワードの保持
final searchQueryProvider = StateProvider<String>((ref) => '');
// 検索結果の取得
final searchResultsProvider = FutureProvider<List<Product>>((ref) async {
final query = ref.watch(searchQueryProvider);
if (query.isEmpty) return [];
// 毎回APIを呼び出してしまう
return await api.searchProducts(query);
});
class SearchScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final results = ref.watch(searchResultsProvider);
return Column(
children: [
TextField(
onChanged: (value) {
// 入力の度にStateProviderを更新
ref.read(searchQueryProvider.notifier).state = value;
},
),
Expanded(
child: results.when(
data: (data) => ListView.builder(
itemCount: data.length,
itemBuilder: (context, index) => ListTile(
title: Text(data[index].name),
),
),
loading: () => CircularProgressIndicator(),
error: (_, __) => Text('エラーが発生しました'),
),
),
],
);
}
}
✅ 正解: 最適化された検索実装
// 検索の状態管理
class SearchState with _$SearchState {
const factory SearchState({
required String query,
required bool isLoading,
required List<Product> results,
required List<Product> recentSearches,
String? error,
}) = _SearchState;
factory SearchState.initial() => SearchState(
query: '',
isLoading: false,
results: [],
recentSearches: [],
);
}
class SearchNotifier extends StateNotifier<SearchState> {
final SearchRepository _repository;
final AnalyticsService _analytics;
Timer? _debounceTimer;
SearchNotifier(this._repository, this._analytics)
: super(SearchState.initial());
Future<void> onQueryChanged(String query) async {
state = state.copyWith(query: query);
_debounceTimer?.cancel();
if (query.isEmpty) {
state = state.copyWith(results: [], isLoading: false);
return;
}
// デバウンス処理
_debounceTimer = Timer(const Duration(milliseconds: 300), () async {
await _performSearch(query);
});
}
Future<void> _performSearch(String query) async {
if (query != state.query) return; // クエリが変更された場合は処理をスキップ
state = state.copyWith(isLoading: true, error: null);
try {
final results = await _repository.searchProducts(
query: query,
limit: 20,
offset: 0,
);
if (!mounted) return;
_analytics.logSearch(query, results.length);
state = state.copyWith(
results: results,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: '検索に失敗しました',
);
}
}
void addToRecentSearches(Product product) {
final currentRecent = List<Product>.from(state.recentSearches);
currentRecent.remove(product); // 重複を削除
currentRecent.insert(0, product); // 先頭に追加
// 最大10件まで保持
if (currentRecent.length > 10) {
currentRecent.removeLast();
}
state = state.copyWith(recentSearches: currentRecent);
}
void dispose() {
_debounceTimer?.cancel();
super.dispose();
}
}
// UIの実装
class SearchScreen extends ConsumerStatefulWidget {
_SearchScreenState createState() => _SearchScreenState();
}
class _SearchScreenState extends ConsumerState<SearchScreen> {
final _searchController = TextEditingController();
void dispose() {
_searchController.dispose();
super.dispose();
}
Widget build(BuildContext context) {
final searchState = ref.watch(searchProvider);
return Scaffold(
appBar: AppBar(
title: TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: '検索...',
border: InputBorder.none,
suffixIcon: searchState.query.isNotEmpty
? IconButton(
icon: Icon(Icons.clear),
onPressed: () {
_searchController.clear();
ref.read(searchProvider.notifier).onQueryChanged('');
},
)
: null,
),
onChanged: (value) {
ref.read(searchProvider.notifier).onQueryChanged(value);
},
),
),
body: Column(
children: [
if (searchState.isLoading)
LinearProgressIndicator(),
if (searchState.error != null)
ErrorBanner(
message: searchState.error!,
onDismiss: () {
ref.read(searchProvider.notifier).clearError();
},
),
Expanded(
child: searchState.query.isEmpty
? _buildRecentSearches(searchState.recentSearches)
: _buildSearchResults(searchState.results),
),
],
),
);
}
Widget _buildRecentSearches(List<Product> recentSearches) {
if (recentSearches.isEmpty) {
return Center(
child: Text('最近の検索履歴はありません'),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16),
child: Text(
'最近の検索',
style: Theme.of(context).textTheme.titleMedium,
),
),
Expanded(
child: ListView.builder(
itemCount: recentSearches.length,
itemBuilder: (context, index) {
final product = recentSearches[index];
return ListTile(
leading: CachedNetworkImage(
imageUrl: product.imageUrl,
placeholder: (context, url) => Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
width: 50,
height: 50,
color: Colors.white,
),
),
),
title: Text(product.name),
subtitle: Text(
'¥${product.price.toStringAsFixed(0)}',
style: TextStyle(color: Theme.of(context).primaryColor),
),
onTap: () {
_searchController.text = product.name;
ref.read(searchProvider.notifier).onQueryChanged(product.name);
},
);
},
),
),
],
);
}
Widget _buildSearchResults(List<Product> results) {
if (results.isEmpty && !searchState.isLoading) {
return Center(
child: Text('検索結果が見つかりません'),
);
}
return ListView.builder(
itemCount: results.length,
itemBuilder: (context, index) {
final product = results[index];
return ProductListItem(
product: product,
onTap: () {
ref.read(searchProvider.notifier).addToRecentSearches(product);
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ProductDetailScreen(product: product),
),
);
},
);
},
);
}
}
// 検索結果アイテムの表示
class ProductListItem extends StatelessWidget {
final Product product;
final VoidCallback onTap;
const ProductListItem({
required this.product,
required this.onTap,
});
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(16),
child: Row(
children: [
CachedNetworkImage(
imageUrl: product.imageUrl,
width: 80,
height: 80,
fit: BoxFit.cover,
),
SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
product.name,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 4),
Text(
'¥${product.price.toStringAsFixed(0)}',
style: TextStyle(
color: Theme.of(context).primaryColor,
fontSize: 14,
),
),
if (product.description != null) ...[
SizedBox(height: 4),
Text(
product.description!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
),
),
],
],
),
),
],
),
),
),
);
}
}
このパターンの主なポイントは:
-
状態管理
- デバウンス処理による API コール最適化
- ローディング状態の適切な管理
- エラーハンドリング
- 検索履歴の管理
-
UI/UX
- クリアボタンの表示
- ローディングインジケータ
- エラーメッセージの表示
- 検索履歴の表示と再検索機能
- 画像のプレースホルダー表示
-
パフォーマンス最適化
- 不必要な API コールの防止
- 画像のキャッシュ
- リスト表示の最適化
-
エラーハンドリング
- ネットワークエラーの処理
- ユーザーへのフィードバック
- リトライ機能
このような実装により、より使いやすく、パフォーマンスの良い検索機能を提供することができます。
Riverpodの実践的な実装パターン - 無限スクロール&ページネーション
1. 無限スクロールの実装
❌ 間違い: 単純な無限スクロール実装
final postsProvider = StateNotifierProvider<PostsNotifier, List<Post>>((ref) {
return PostsNotifier();
});
class PostsNotifier extends StateNotifier<List<Post>> {
int _page = 1;
bool _isLoading = false;
PostsNotifier() : super([]) {
loadMore();
}
Future<void> loadMore() async {
if (_isLoading) return;
_isLoading = true;
final posts = await api.getPosts(page: _page);
state = [...state, ...posts];
_page++;
_isLoading = false;
}
}
class PostsScreen extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final posts = ref.watch(postsProvider);
return ListView.builder(
itemCount: posts.length,
itemBuilder: (context, index) {
if (index == posts.length - 1) {
ref.read(postsProvider.notifier).loadMore();
}
return ListTile(title: Text(posts[index].title));
},
);
}
}
✅ 正解: 最適化された無限スクロール実装
// ページネーション状態の定義
class PaginationState<T> with _$PaginationState<T> {
const factory PaginationState({
required List<T> items,
required bool isLoading,
required bool hasNextPage,
required int currentPage,
String? error,
}) = _PaginationState;
factory PaginationState.initial() => PaginationState(
items: [],
isLoading: false,
hasNextPage: true,
currentPage: 1,
);
}
// ページネーション用のNotifier
class PaginatedPostsNotifier extends StateNotifier<PaginationState<Post>> {
final PostRepository _repository;
final int _pageSize;
bool _isLoadingMore = false;
PaginatedPostsNotifier(this._repository, {int pageSize = 20})
: _pageSize = pageSize,
super(PaginationState.initial()) {
loadInitial();
}
Future<void> loadInitial() async {
state = state.copyWith(isLoading: true, error: null);
try {
final response = await _repository.getPosts(
page: 1,
pageSize: _pageSize,
);
state = state.copyWith(
items: response.items,
hasNextPage: response.items.length >= _pageSize,
currentPage: 1,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: '投稿の読み込みに失敗しました',
);
}
}
Future<void> loadMore() async {
if (_isLoadingMore || !state.hasNextPage) return;
if (state.error != null) return;
_isLoadingMore = true;
state = state.copyWith(isLoading: true, error: null);
try {
final nextPage = state.currentPage + 1;
final response = await _repository.getPosts(
page: nextPage,
pageSize: _pageSize,
);
state = state.copyWith(
items: [...state.items, ...response.items],
hasNextPage: response.items.length >= _pageSize,
currentPage: nextPage,
isLoading: false,
);
} catch (e) {
state = state.copyWith(
isLoading: false,
error: '追加の投稿の読み込みに失敗しました',
);
} finally {
_isLoadingMore = false;
}
}
Future<void> refresh() async {
state = PaginationState.initial();
await loadInitial();
}
}
// UIの実装
class PostsScreen extends ConsumerStatefulWidget {
_PostsScreenState createState() => _PostsScreenState();
}
class _PostsScreenState extends ConsumerState<PostsScreen> {
final _scrollController = ScrollController();
void initState() {
super.initState();
_scrollController.addListener(_onScroll);
}
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_isBottom) {
ref.read(paginatedPostsProvider.notifier).loadMore();
}
}
bool get _isBottom {
if (!_scrollController.hasClients) return false;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.offset;
return currentScroll >= (maxScroll * 0.9); // 90%スクロールで次を読み込み
}
Widget build(BuildContext context) {
final state = ref.watch(paginatedPostsProvider);
return Scaffold(
appBar: AppBar(
title: Text('投稿一覧'),
actions: [
IconButton(
icon: Icon(Icons.refresh),
onPressed: () {
ref.read(paginatedPostsProvider.notifier).refresh();
},
),
],
),
body: RefreshIndicator(
onRefresh: () async {
await ref.read(paginatedPostsProvider.notifier).refresh();
},
child: CustomScrollView(
controller: _scrollController,
slivers: [
if (state.error != null)
SliverToBoxAdapter(
child: ErrorBanner(
message: state.error!,
onRetry: () {
ref.read(paginatedPostsProvider.notifier).loadMore();
},
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index >= state.items.length) {
if (state.isLoading) {
return _buildLoader();
}
if (!state.hasNextPage) {
return _buildNoMoreContent();
}
return null;
}
return PostListItem(
post: state.items[index],
onTap: () => _navigateToDetail(state.items[index]),
);
},
childCount: state.items.length + (state.hasNextPage ? 1 : 0),
),
),
],
),
),
);
}
Widget _buildLoader() {
return Container(
padding: EdgeInsets.symmetric(vertical: 16),
alignment: Alignment.center,
child: CircularProgressIndicator(),
);
}
Widget _buildNoMoreContent() {
return Container(
padding: EdgeInsets.symmetric(vertical: 32),
alignment: Alignment.center,
child: Text(
'全ての投稿を読み込みました',
style: TextStyle(
color: Colors.grey,
fontSize: 14,
),
),
);
}
void _navigateToDetail(Post post) {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => PostDetailScreen(post: post),
),
);
}
}
// 投稿アイテムのUI
class PostListItem extends StatelessWidget {
final Post post;
final VoidCallback onTap;
const PostListItem({
required this.post,
required this.onTap,
});
Widget build(BuildContext context) {
return Card(
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: onTap,
child: Padding(
padding: EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
backgroundImage: NetworkImage(post.author.avatarUrl),
),
SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
post.author.name,
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
Text(
_formatDate(post.createdAt),
style: TextStyle(
color: Colors.grey,
fontSize: 12,
),
),
],
),
],
),
SizedBox(height: 12),
Text(
post.title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
if (post.content != null) ...[
SizedBox(height: 8),
Text(
post.content!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
if (post.imageUrl != null) ...[
SizedBox(height: 12),
CachedNetworkImage(
imageUrl: post.imageUrl!,
placeholder: (context, url) => Shimmer.fromColors(
baseColor: Colors.grey[300]!,
highlightColor: Colors.grey[100]!,
child: Container(
height: 200,
color: Colors.white,
),
),
errorWidget: (context, url, error) => Icon(Icons.error),
fit: BoxFit.cover,
),
],
SizedBox(height: 12),
Row(
children: [
Icon(Icons.favorite_border, size: 16),
SizedBox(width: 4),
Text('${post.likesCount}'),
SizedBox(width: 16),
Icon(Icons.comment_outlined, size: 16),
SizedBox(width: 4),
Text('${post.commentsCount}'),
],
),
],
),
),
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays > 7) {
return DateFormat.yMMMd().format(date);
} else if (difference.inDays > 0) {
return '${difference.inDays}日前';
} else if (difference.inHours > 0) {
return '${difference.inHours}時間前';
} else if (difference.inMinutes > 0) {
return '${difference.inMinutes}分前';
} else {
return 'たった今';
}
}
}
このパターンの主なポイントは:
-
状態管理
- ページネーション状態の完全な管理
- ローディング状態の適切な制御
- エラーハンドリング
- リフレッシュ機能
-
UI/UX
- スムーズなスクロール体験
- ローディングインジケータ
- エラーメッセージの表示
- リフレッシュ機能
- 最後までスクロールした時の表示
-
パフォーマンス最適化
- スクロール位置の適切な検出
- 画像のレイジーローディング
- 適切なキャッシュ管理
-
エラーハンドリング
- ネットワークエラーの処理
- 再試行機能
- ユーザーへのフィードバック
このような実装により、スムーズで使いやすい無限スクロール機能を提供することができます。