20 KiB
Riverpod 3.0 State Management - Complete Documentation
Overview
This document provides comprehensive documentation for all Riverpod 3.0 providers in the Retail POS application. All providers use code generation with the @riverpod annotation.
1. Cart Management Providers
Location: /lib/features/home/presentation/providers/
1.1 CartProvider (cart_provider.dart)
Purpose: Manages shopping cart state with full CRUD operations.
Type: Notifier<List<CartItem>>
State: List<CartItem>
Methods:
addItem(Product product, int quantity)- Add product to cart or update quantityremoveItem(String productId)- Remove item from cartupdateQuantity(String productId, int newQuantity)- Update item quantityincrementQuantity(String productId)- Increase quantity by 1decrementQuantity(String productId)- Decrease quantity by 1clearCart()- Clear all itemscontainsProduct(String productId)- Check if product existsgetProductQuantity(String productId)- Get current quantity
Usage:
// Watch cart state
final cartItems = ref.watch(cartProvider);
// Add to cart
ref.read(cartProvider.notifier).addItem(product, 1);
// Update quantity
ref.read(cartProvider.notifier).updateQuantity('product-1', 3);
// Clear cart
ref.read(cartProvider.notifier).clearCart();
1.2 CartTotalProvider (cart_total_provider.dart)
Purpose: Calculates cart totals including subtotal, tax, and total.
Type: Notifier<CartTotalData>
State: CartTotalData (subtotal, tax, taxRate, total, itemCount)
Dependencies:
cartProvider- For cart itemssettingsProvider- For tax rate
Methods:
applyDiscount(double discountAmount)- Calculate total with flat discountapplyDiscountPercentage(double discountPercent)- Calculate total with percentage discount
Usage:
// Watch cart totals
final totals = ref.watch(cartTotalProvider);
// Access values
print('Subtotal: ${totals.subtotal}');
print('Tax: ${totals.tax}');
print('Total: ${totals.total}');
print('Items: ${totals.itemCount}');
// Apply discount
final discounted = ref.read(cartTotalProvider.notifier).applyDiscount(10.0);
1.3 CartItemCountProvider (cart_item_count_provider.dart)
Purpose: Provides optimized cart item counts.
Type: Function provider (computed value)
Providers:
cartItemCount- Total quantity of all itemscartUniqueItemCount- Number of unique products
Usage:
// Total items quantity
final totalItems = ref.watch(cartItemCountProvider);
// Unique items count
final uniqueItems = ref.watch(cartUniqueItemCountProvider);
2. Products Management Providers
Location: /lib/features/products/presentation/providers/
2.1 ProductLocalDataSourceProvider (product_datasource_provider.dart)
Purpose: Dependency injection for product data source.
Type: Provider<ProductLocalDataSource> (keepAlive)
Usage:
final dataSource = ref.read(productLocalDataSourceProvider);
2.2 ProductsProvider (products_provider.dart)
Purpose: Fetches and manages all products from Hive.
Type: AsyncNotifier<List<Product>>
State: AsyncValue<List<Product>>
Methods:
refresh()- Refresh products from data sourcesyncProducts()- Sync with remote APIgetProductById(String id)- Get specific product
Usage:
// Watch products
final productsAsync = ref.watch(productsProvider);
productsAsync.when(
data: (products) => ProductGrid(products: products),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error),
);
// Refresh products
await ref.read(productsProvider.notifier).refresh();
// Sync products
await ref.read(productsProvider.notifier).syncProducts();
2.3 SearchQueryProvider (search_query_provider.dart)
Purpose: Manages product search query state.
Type: Notifier<String>
State: String (search query)
Methods:
setQuery(String query)- Update search queryclear()- Clear searchisSearching- Check if search is active
Usage:
// Watch search query
final query = ref.watch(searchQueryProvider);
// Update search
ref.read(searchQueryProvider.notifier).setQuery('laptop');
// Clear search
ref.read(searchQueryProvider.notifier).clear();
2.4 SelectedCategoryProvider (selected_category_provider.dart)
Purpose: Manages selected category filter.
Type: Notifier<String?>
State: String? (category ID or null for all)
Methods:
selectCategory(String? categoryId)- Select categoryclearSelection()- Clear filter (show all)hasSelection- Check if category selectedisSelected(String categoryId)- Check specific category
Usage:
// Watch selected category
final categoryId = ref.watch(selectedCategoryProvider);
// Select category
ref.read(selectedCategoryProvider.notifier).selectCategory('electronics');
// Clear filter
ref.read(selectedCategoryProvider.notifier).clearSelection();
2.5 FilteredProductsProvider (filtered_products_provider.dart)
Purpose: Combines search and category filtering.
Type: Notifier<List<Product>>
State: List<Product> (filtered results)
Dependencies:
productsProvider- All productssearchQueryProvider- Search queryselectedCategoryProvider- Category filter
Getters:
availableCount- Count of available productsoutOfStockCount- Count of out-of-stock products
Usage:
// Watch filtered products (automatically updates when filters change)
final filtered = ref.watch(filteredProductsProvider);
// Get counts
final available = ref.read(filteredProductsProvider.notifier).availableCount;
2.6 SortedProductsProvider (filtered_products_provider.dart)
Purpose: Sorts filtered products by various options.
Type: Notifier<List<Product>> with family parameter
Parameters: ProductSortOption enum
Sort Options:
nameAsc- Name A-ZnameDesc- Name Z-ApriceAsc- Price low to highpriceDesc- Price high to lownewest- Newest firstoldest- Oldest first
Usage:
// Watch sorted products
final sorted = ref.watch(sortedProductsProvider(ProductSortOption.priceAsc));
3. Categories Management Providers
Location: /lib/features/categories/presentation/providers/
3.1 CategoryLocalDataSourceProvider (category_datasource_provider.dart)
Purpose: Dependency injection for category data source.
Type: Provider<CategoryLocalDataSource> (keepAlive)
3.2 CategoriesProvider (categories_provider.dart)
Purpose: Fetches and manages all categories.
Type: AsyncNotifier<List<Category>>
State: AsyncValue<List<Category>>
Methods:
refresh()- Refresh categoriessyncCategories()- Sync with remote APIgetCategoryById(String id)- Get categorygetCategoryName(String id)- Get category name
Usage:
// Watch categories
final categoriesAsync = ref.watch(categoriesProvider);
categoriesAsync.when(
data: (categories) => CategoryGrid(categories: categories),
loading: () => CircularProgressIndicator(),
error: (error, stack) => ErrorWidget(error),
);
// Get category name
final name = ref.read(categoriesProvider.notifier).getCategoryName('electronics');
3.3 CategoryProductCountProvider (category_product_count_provider.dart)
Purpose: Calculates product count per category.
Type: Function provider with family parameter
Providers:
categoryProductCount(String categoryId)- Count for specific categoryallCategoryProductCounts- Map of all counts
Usage:
// Watch count for specific category
final count = ref.watch(categoryProductCountProvider('electronics'));
// Watch all counts
final allCounts = ref.watch(allCategoryProductCountsProvider);
print('Electronics: ${allCounts['electronics']}');
4. Settings Management Providers
Location: /lib/features/settings/presentation/providers/
4.1 SettingsLocalDataSourceProvider (settings_datasource_provider.dart)
Purpose: Dependency injection for settings data source.
Type: Provider<SettingsLocalDataSource> (keepAlive)
4.2 SettingsProvider (settings_provider.dart)
Purpose: Manages all app settings.
Type: AsyncNotifier<AppSettings> (keepAlive)
State: AsyncValue<AppSettings>
Methods:
updateThemeMode(ThemeMode mode)- Update themeupdateLanguage(String language)- Update languageupdateTaxRate(double taxRate)- Update tax rateupdateStoreName(String storeName)- Update store nameupdateCurrency(String currency)- Update currencytoggleSync()- Toggle sync on/offupdateLastSyncTime()- Update sync timestampresetToDefaults()- Reset all settings
Usage:
// Watch settings
final settingsAsync = ref.watch(settingsProvider);
// Update theme
await ref.read(settingsProvider.notifier).updateThemeMode(ThemeMode.dark);
// Update tax rate
await ref.read(settingsProvider.notifier).updateTaxRate(0.08);
// Reset settings
await ref.read(settingsProvider.notifier).resetToDefaults();
4.3 ThemeProvider (theme_provider.dart)
Purpose: Extracts theme-related data from settings.
Type: Function providers (computed values)
Providers:
themeModeProvider- Current theme modeisDarkModeProvider- Check if dark modeisLightModeProvider- Check if light modeisSystemThemeProvider- Check if system theme
Usage:
// Watch theme mode
final theme = ref.watch(themeModeProvider);
// Check dark mode
final isDark = ref.watch(isDarkModeProvider);
// Use in MaterialApp
MaterialApp(
themeMode: ref.watch(themeModeProvider),
// ...
)
4.4 LanguageProvider (language_provider.dart)
Purpose: Manages language/locale settings.
Type: Function providers
Providers:
appLanguageProvider- Current language codesupportedLanguagesProvider- List of available languages
Model: LanguageOption (code, name, nativeName)
Usage:
// Watch current language
final language = ref.watch(appLanguageProvider);
// Get supported languages
final languages = ref.watch(supportedLanguagesProvider);
// Display language selector
DropdownButton(
value: language,
items: languages.map((lang) => DropdownMenuItem(
value: lang.code,
child: Text(lang.nativeName),
)).toList(),
onChanged: (code) => ref.read(settingsProvider.notifier).updateLanguage(code!),
)
5. Core Providers
Location: /lib/core/providers/
5.1 NetworkInfoProvider (network_info_provider.dart)
Purpose: Network connectivity management.
Type: Multiple providers
Providers:
connectivityProvider- Connectivity instance (keepAlive)networkInfoProvider- NetworkInfo implementation (keepAlive)isConnectedProvider- Check if connectedconnectivityStreamProvider- Stream of connectivity changes
Usage:
// Check if connected
final isConnected = await ref.read(isConnectedProvider.future);
// Watch connectivity changes
ref.listen(connectivityStreamProvider, (previous, next) {
next.when(
data: (connected) {
if (connected) {
print('Connected to internet');
} else {
print('Offline');
}
},
loading: () {},
error: (e, s) {},
);
});
5.2 SyncStatusProvider (sync_status_provider.dart)
Purpose: Manages data synchronization state.
Type: AsyncNotifier<SyncResult>
State: AsyncValue<SyncResult>
Models:
SyncResult(status, lastSyncTime, message, error)SyncStateenum (idle, syncing, success, failed, offline)
Methods:
syncAll()- Sync all data (categories + products)syncProducts()- Sync only productssyncCategories()- Sync only categoriesresetStatus()- Reset to idle state
Getters:
isSyncing- Check if syncingisSuccess- Check if successfulisFailed- Check if failedisOffline- Check if offlineisIdle- Check if idle
Additional Providers:
lastSyncTimeProvider- Get last sync time from settings
Usage:
// Watch sync status
final syncAsync = ref.watch(syncStatusProvider);
syncAsync.when(
data: (result) {
if (result.isSyncing) {
return CircularProgressIndicator();
} else if (result.isSuccess) {
return Text('Synced: ${result.lastSyncTime}');
} else if (result.isFailed) {
return Text('Error: ${result.message}');
}
return SyncButton();
},
loading: () => CircularProgressIndicator(),
error: (e, s) => ErrorWidget(e),
);
// Trigger sync
await ref.read(syncStatusProvider.notifier).syncAll();
// Sync only products
await ref.read(syncStatusProvider.notifier).syncProducts();
// Get last sync time
final lastSync = ref.watch(lastSyncTimeProvider);
Provider Dependencies Graph
┌─────────────────────────────────────────────────────────────┐
│ CORE PROVIDERS │
├─────────────────────────────────────────────────────────────┤
│ - networkInfoProvider (keepAlive) │
│ - connectivityProvider (keepAlive) │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ SYNC STATUS PROVIDER │
├─────────────────────────────────────────────────────────────┤
│ Depends on: networkInfo, products, categories, settings │
└─────────────────────────────────────────────────────────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ PRODUCTS │ │ CATEGORIES │ │ SETTINGS │
│ - products │ │ - categories │ │ - settings │
│ (async) │ │ (async) │ │ (async) │
└──────────────────┘ └──────────────┘ └──────────────┘
│ │ │
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ FILTERED │ │ CATEGORY │ │ THEME │
│ PRODUCTS │ │ COUNTS │ │ LANGUAGE │
│ - search query │ │ - product │ │ - theme mode │
│ - selected cat │ │ counts │ │ - language │
│ - filtered list │ │ │ │ │
└──────────────────┘ └──────────────┘ └──────────────┘
│
▼
┌──────────────────┐
│ CART │
│ - cart items │
│ - cart total │
│ - item count │
└──────────────────┘
Code Generation
Running Code Generator
# One-time build
dart run build_runner build --delete-conflicting-outputs
# Watch mode (auto-rebuild on changes)
dart run build_runner watch --delete-conflicting-outputs
# Clean and rebuild
dart run build_runner clean
dart run build_runner build --delete-conflicting-outputs
Generated Files
Each provider file with @riverpod annotation generates a corresponding .g.dart file:
cart_provider.dart→cart_provider.g.dartproducts_provider.dart→products_provider.g.dartcategories_provider.dart→categories_provider.g.dart- etc.
Best Practices
1. Use .select() for Performance
// Bad - rebuilds on any cart change
final cart = ref.watch(cartProvider);
// Good - rebuilds only when length changes
final itemCount = ref.watch(cartProvider.select((items) => items.length));
2. Use AsyncValue Properly
// Use .when() for simple cases
productsAsync.when(
data: (data) => Text('$data'),
loading: () => Loading(),
error: (e, s) => Error(e),
);
// Use pattern matching for complex cases
switch (productsAsync) {
case AsyncData(:final value):
return ProductList(value);
case AsyncError(:final error):
return ErrorWidget(error);
case AsyncLoading():
return LoadingWidget();
}
3. Check ref.mounted in Async Operations
Future<void> updateData() async {
state = const AsyncValue.loading();
await someAsyncOperation();
// Always check if still mounted
if (!ref.mounted) return;
state = AsyncValue.data(result);
}
4. Use keepAlive for Dependencies
// Dependency injection providers should be keepAlive
@Riverpod(keepAlive: true)
ProductDataSource productDataSource(Ref ref) {
return ProductDataSourceImpl();
}
5. Invalidate vs Refresh
// Invalidate - reset provider to initial state
ref.invalidate(productsProvider);
// Refresh - invalidate and immediately read
final products = ref.refresh(productsProvider);
Testing Providers
Unit Testing Example
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
test('Cart adds items correctly', () {
final container = ProviderContainer();
addTearDown(container.dispose);
// Initial state
expect(container.read(cartProvider), isEmpty);
// Add item
container.read(cartProvider.notifier).addItem(mockProduct, 1);
// Verify
expect(container.read(cartProvider).length, 1);
expect(container.read(cartItemCountProvider), 1);
});
test('Filtered products work correctly', () async {
final container = ProviderContainer();
addTearDown(container.dispose);
// Wait for products to load
await container.read(productsProvider.future);
// Set search query
container.read(searchQueryProvider.notifier).setQuery('laptop');
// Check filtered results
final filtered = container.read(filteredProductsProvider);
expect(filtered.every((p) => p.name.toLowerCase().contains('laptop')), true);
});
}
Summary
Total Providers Created: 25+
By Feature:
- Cart: 3 providers
- Products: 5 providers
- Categories: 3 providers
- Settings: 4 providers
- Core/Sync: 5+ providers
By Type:
- AsyncNotifier: 4 (products, categories, settings, sync)
- Notifier: 4 (cart, searchQuery, selectedCategory, filteredProducts)
- Function Providers: 10+ (counts, theme, language, etc.)
- Dependency Injection: 4 (data sources, network info)
All providers follow Riverpod 3.0 best practices with code generation!