Files
worker/lib/features/favorites/USAGE_EXAMPLES.md
Phuoc Nguyen 19d9a3dc2d update loaing
2025-12-02 18:09:20 +07:00

18 KiB

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

// 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

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

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

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

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

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 const CustomLoadingIndicator(),
      error: (error, stack) => Text('Error: $error'),
    );
  }
}

6. Refresh Favorites from Database

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

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

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:

// 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

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:

    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