This commit is contained in:
Phuoc Nguyen
2025-10-24 16:20:48 +07:00
parent eaaa9921f5
commit b27c5d7742
17 changed files with 3245 additions and 5 deletions

View File

@@ -0,0 +1,640 @@
# Favorites Provider - Usage Examples
This document provides practical examples of how to use the Favorites state management provider in the Worker app.
## Overview
The Favorites feature provides a complete state management solution for managing user's favorite products using Riverpod 3.0 with code generation and Hive for local persistence.
## Provider Structure
```dart
// Main providers
favoritesProvider // AsyncNotifier<Set<String>>
favoritesLocalDataSourceProvider // FavoritesLocalDataSource
currentUserIdProvider // String (TODO: replace with auth)
// Helper providers
isFavoriteProvider(productId) // bool
favoriteCountProvider // int
favoriteProductIdsProvider // List<String>
```
---
## Basic Usage Examples
### 1. Display Favorite Button in Product Card
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
class ProductCard extends ConsumerWidget {
final String productId;
final String productName;
final double price;
const ProductCard({
super.key,
required this.productId,
required this.productName,
required this.price,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch if this product is favorited
final isFavorited = ref.watch(isFavoriteProvider(productId));
return Card(
child: Column(
children: [
// Product image and details
Text(productName),
Text('\$$price'),
// Favorite button
IconButton(
icon: Icon(
isFavorited ? Icons.favorite : Icons.favorite_border,
color: isFavorited ? Colors.red : Colors.grey,
),
onPressed: () {
// Toggle favorite status
ref.read(favoritesProvider.notifier).toggleFavorite(productId);
},
),
],
),
);
}
}
```
---
### 2. Display Favorite Count Badge
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
class FavoritesIconWithBadge extends ConsumerWidget {
const FavoritesIconWithBadge({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Watch favorite count
final favoriteCount = ref.watch(favoriteCountProvider);
return Stack(
children: [
const Icon(Icons.favorite, size: 32),
if (favoriteCount > 0)
Positioned(
right: 0,
top: 0,
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.red,
shape: BoxShape.circle,
),
constraints: const BoxConstraints(
minWidth: 20,
minHeight: 20,
),
child: Text(
'$favoriteCount',
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.bold,
),
textAlign: TextAlign.center,
),
),
),
],
);
}
}
```
---
### 3. Favorites Page - Display All Favorites
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart';
class FavoritesPage extends ConsumerWidget {
const FavoritesPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final favoritesAsync = ref.watch(favoritesProvider);
final productsAsync = ref.watch(productsProvider);
return Scaffold(
appBar: AppBar(
title: const Text('My Favorites'),
actions: [
// Clear all button
IconButton(
icon: const Icon(Icons.delete_sweep),
onPressed: () {
_showClearAllDialog(context, ref);
},
),
],
),
body: favoritesAsync.when(
data: (favoriteIds) {
if (favoriteIds.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.favorite_border, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'No favorites yet',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'Add products to your favorites to see them here',
style: TextStyle(color: Colors.grey),
),
],
),
);
}
return productsAsync.when(
data: (products) {
// Filter products to show only favorites
final favoriteProducts = products
.where((product) => favoriteIds.contains(product.id))
.toList();
return GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.75,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: favoriteProducts.length,
itemBuilder: (context, index) {
final product = favoriteProducts[index];
return ProductCard(
productId: product.id,
productName: product.name,
price: product.price,
);
},
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 64, color: Colors.red),
const SizedBox(height: 16),
Text('Error loading favorites: $error'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
ref.invalidate(favoritesProvider);
},
child: const Text('Retry'),
),
],
),
),
),
);
}
void _showClearAllDialog(BuildContext context, WidgetRef ref) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear All Favorites'),
content: const Text('Are you sure you want to remove all favorites?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
ref.read(favoritesProvider.notifier).clearAll();
Navigator.pop(context);
},
child: const Text('Clear', style: TextStyle(color: Colors.red)),
),
],
),
);
}
}
```
---
### 4. Add/Remove Favorites Programmatically
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
class ProductDetailPage extends ConsumerWidget {
final String productId;
const ProductDetailPage({super.key, required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isFavorited = ref.watch(isFavoriteProvider(productId));
return Scaffold(
appBar: AppBar(
title: const Text('Product Details'),
actions: [
IconButton(
icon: Icon(
isFavorited ? Icons.favorite : Icons.favorite_border,
color: isFavorited ? Colors.red : Colors.white,
),
onPressed: () async {
final notifier = ref.read(favoritesProvider.notifier);
if (isFavorited) {
await notifier.removeFavorite(productId);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Removed from favorites')),
);
}
} else {
await notifier.addFavorite(productId);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Added to favorites')),
);
}
}
},
),
],
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('Product Details'),
const SizedBox(height: 32),
// Custom button with loading state
ElevatedButton.icon(
icon: Icon(isFavorited ? Icons.favorite : Icons.favorite_border),
label: Text(isFavorited ? 'Remove from Favorites' : 'Add to Favorites'),
onPressed: () {
ref.read(favoritesProvider.notifier).toggleFavorite(productId);
},
),
],
),
),
);
}
}
```
---
### 5. Filter Products to Show Only Favorites
```dart
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart';
// Create a derived provider for favorite products
@riverpod
Future<List<Product>> favoriteProducts(Ref ref) async {
// Get all products
final products = await ref.watch(productsProvider.future);
// Get favorite IDs
final favoriteIds = await ref.watch(favoritesProvider.future);
// Filter products
return products.where((p) => favoriteIds.contains(p.id)).toList();
}
// Usage in widget
class FavoriteProductsList extends ConsumerWidget {
const FavoriteProductsList({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final favoriteProductsAsync = ref.watch(favoriteProductsProvider);
return favoriteProductsAsync.when(
data: (products) => ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final product = products[index];
return ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
);
},
),
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
}
```
---
### 6. Refresh Favorites from Database
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
class FavoritesPageWithRefresh extends ConsumerWidget {
const FavoritesPageWithRefresh({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final favoritesAsync = ref.watch(favoritesProvider);
return Scaffold(
appBar: AppBar(
title: const Text('My Favorites'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: () {
// Refresh favorites from database
ref.read(favoritesProvider.notifier).refresh();
},
),
],
),
body: RefreshIndicator(
onRefresh: () async {
// Pull to refresh
await ref.read(favoritesProvider.notifier).refresh();
},
child: favoritesAsync.when(
data: (favorites) => ListView.builder(
itemCount: favorites.length,
itemBuilder: (context, index) {
final productId = favorites.elementAt(index);
return ListTile(title: Text('Product: $productId'));
},
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')),
),
),
);
}
}
```
---
## Advanced Usage
### 7. Handle Loading States with Custom UI
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
class FavoriteButtonWithLoadingState extends ConsumerWidget {
final String productId;
const FavoriteButtonWithLoadingState({
super.key,
required this.productId,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final favoritesAsync = ref.watch(favoritesProvider);
return favoritesAsync.when(
data: (favorites) {
final isFavorited = favorites.contains(productId);
return IconButton(
icon: Icon(
isFavorited ? Icons.favorite : Icons.favorite_border,
color: isFavorited ? Colors.red : Colors.grey,
),
onPressed: () {
ref.read(favoritesProvider.notifier).toggleFavorite(productId);
},
);
},
loading: () => const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(strokeWidth: 2),
),
error: (error, stack) => IconButton(
icon: const Icon(Icons.error, color: Colors.grey),
onPressed: () {
ref.invalidate(favoritesProvider);
},
),
);
}
}
```
---
### 8. Sync Favorites on App Resume
```dart
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
class FavoritesAppLifecycleManager extends ConsumerStatefulWidget {
final Widget child;
const FavoritesAppLifecycleManager({
super.key,
required this.child,
});
@override
ConsumerState<FavoritesAppLifecycleManager> createState() =>
_FavoritesAppLifecycleManagerState();
}
class _FavoritesAppLifecycleManagerState
extends ConsumerState<FavoritesAppLifecycleManager>
with WidgetsBindingObserver {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.resumed) {
// Refresh favorites when app resumes
ref.read(favoritesProvider.notifier).refresh();
}
}
@override
Widget build(BuildContext context) {
return widget.child;
}
}
```
---
## Integration with Auth Provider (TODO)
Once the authentication provider is available, update the `currentUserId` provider:
```dart
// In favorites_provider.dart
/// Provides the current logged-in user's ID
@riverpod
String currentUserId(Ref ref) {
// Replace this with actual auth integration:
final authState = ref.watch(authProvider);
return authState.user?.id ?? 'guest';
// Or throw an error if user is not logged in:
// final user = ref.watch(authProvider).user;
// if (user == null) throw Exception('User not logged in');
// return user.id;
}
```
---
## Testing Examples
### Unit Test Example
```dart
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
void main() {
test('should add favorite to state', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
// Initial state should be empty
final initialState = await container.read(favoritesProvider.future);
expect(initialState, isEmpty);
// Add favorite
await container.read(favoritesProvider.notifier).addFavorite('product_1');
// Check if favorite was added
final updatedState = await container.read(favoritesProvider.future);
expect(updatedState, contains('product_1'));
});
test('isFavorite should return true for favorited product', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
// Add favorite
await container.read(favoritesProvider.notifier).addFavorite('product_1');
// Check if product is favorited
final isFavorited = container.read(isFavoriteProvider('product_1'));
expect(isFavorited, isTrue);
});
}
```
---
## Performance Tips
1. **Use `isFavoriteProvider` for individual checks** - This prevents rebuilding all favorite-dependent widgets when the favorites list changes.
2. **Use `.select()` for specific data** - If you only need a specific piece of data:
```dart
final hasFavorites = ref.watch(
favoritesProvider.select((async) => async.value?.isNotEmpty ?? false)
);
```
3. **Avoid unnecessary rebuilds** - Wrap expensive widgets with `RepaintBoundary` or use `Consumer` to isolate rebuilds.
4. **Batch operations** - If adding/removing multiple favorites, consider implementing a batch operation method.
---
## Troubleshooting
### Favorites not persisting
- Ensure Hive box is properly initialized in `main.dart`
- Check that the favorite box is opened: `Hive.openBox<FavoriteModel>(HiveBoxNames.favoriteBox)`
### State not updating
- Verify you're using `ref.read()` for mutations and `ref.watch()` for listening
- Check console logs for error messages
### Performance issues
- Use `isFavoriteProvider` instead of watching the entire `favoritesProvider`
- Implement pagination for large favorite lists
- Consider compacting the Hive box periodically
---
## Next Steps
1. **Integrate with Auth Provider** - Replace hardcoded userId with actual user from auth state
2. **Add Remote Sync** - Implement API calls to sync favorites with backend
3. **Analytics** - Track favorite actions for user insights
4. **Recommendations** - Use favorite data to recommend similar products