# 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> favoritesLocalDataSourceProvider // FavoritesLocalDataSource currentUserIdProvider // String (TODO: replace with auth) // Helper providers isFavoriteProvider(productId) // bool favoriteCountProvider // int favoriteProductIdsProvider // List ``` --- ## 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: const CustomLoadingIndicator()), error: (error, stack) => Center(child: Text('Error: $error')), ); }, loading: () => const Center(child: const CustomLoadingIndicator()), 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> 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 CustomLoadingIndicator(), 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: const CustomLoadingIndicator()), 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: CustomLoadingIndicator(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 createState() => _FavoritesAppLifecycleManagerState(); } class _FavoritesAppLifecycleManagerState extends ConsumerState 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(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