State Management
BookWish uses Riverpod 2.x with code generation for type-safe, scalable state management.
Setup
Dependencies
dependencies:
flutter_riverpod: ^2.4.0
riverpod_annotation: ^2.3.0
dev_dependencies:
riverpod_generator: ^2.3.0
build_runner: ^2.4.0
Provider Scope
The app is wrapped with ProviderScope in /lib/app.dart:
class App extends StatelessWidget {
Widget build(BuildContext context) {
return ProviderScope(
child: Consumer(
builder: (context, ref, _) {
final router = ref.watch(routerProvider);
return MaterialApp.router(
routerConfig: router,
theme: AppTheme.lightTheme,
);
},
),
);
}
}
Code Generation Pattern
All providers use the @riverpod annotation and code generation:
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'example_provider.g.dart';
class ExampleNotifier extends _$ExampleNotifier {
Future<ExampleState> build() async {
// Initialize state
return ExampleState();
}
Future<void> performAction() async {
// Update state
}
}
Run code generation:
flutter pub run build_runner build --delete-conflicting-outputs
Key Providers
Authentication (auth_provider.dart)
Manages user authentication state and tokens.
class AuthNotifier extends _$AuthNotifier {
Future<AuthState> build() async {
_authService = AuthService(ApiClient());
_storage = SecureStorage();
await _initAuth();
return state.value ?? const AuthState();
}
Future<void> login(String email, String password) async { ... }
Future<void> signup(String email, String password, String displayName) async { ... }
Future<void> createGuestSession() async { ... }
Future<void> logout() async { ... }
}
AuthState Structure:
class AuthState {
final User? user;
final String? accessToken;
final String? refreshToken;
final bool isLoading;
bool get isAuthenticated => user != null;
bool get isGuest => user?.isGuest ?? false;
}
Usage:
// Watch auth state
final authState = ref.watch(authNotifierProvider);
// Get current user
final user = authState.value?.user;
// Perform actions
ref.read(authNotifierProvider.notifier).login(email, password);
Wishlists (wishlist_provider.dart)
Manages user wishlists and items.
class WishlistsNotifier extends _$WishlistsNotifier {
Future<List<Wishlist>> build() async {
_service = ref.read(wishlistServiceProvider);
return _service.getWishlists();
}
Future<void> refresh() async { ... }
Future<Wishlist> createWishlist(String name, {...}) async { ... }
Future<void> deleteWishlist(String id) async { ... }
}
// Detail provider for single wishlist
class WishlistDetailNotifier extends _$WishlistDetailNotifier {
Future<Wishlist> build(String wishlistId) async {
_service = ref.read(wishlistServiceProvider);
return _service.getWishlist(wishlistId);
}
Future<void> addItem(Map<String, dynamic> book, String priority) async { ... }
Future<void> removeItem(String itemId) async { ... }
}
Usage:
// Watch all wishlists
final wishlists = ref.watch(wishlistsNotifierProvider);
// Watch specific wishlist
final wishlist = ref.watch(wishlistDetailNotifierProvider(wishlistId));
// Add item to wishlist
await ref.read(wishlistDetailNotifierProvider(id).notifier).addItem(book, 'high');
Cart (cart_provider.dart)
Manages shopping cart state.
class CartState {
final Cart cart;
final CartTotals totals;
final bool isCheckingOut;
int get itemCount => cart.items.length;
bool get isEmpty => cart.items.isEmpty;
}
class CartNotifier extends _$CartNotifier {
Future<CartState> build() async {
_service = ref.read(cartServiceProvider);
final response = await _service.getCartWithTotals();
return CartState(cart: response.cart, totals: response.totals);
}
Future<void> addToCart(String bookId, {int quantity = 1, String? storeId}) async { ... }
Future<void> updateQuantity(String itemId, int quantity) async { ... }
Future<void> removeItem(String itemId) async { ... }
Future<CheckoutResult> checkout({...}) async { ... }
}
Usage:
// Watch cart
final cartState = ref.watch(cartNotifierProvider);
final itemCount = cartState.value?.itemCount ?? 0;
// Add to cart
await ref.read(cartNotifierProvider.notifier).addToCart(bookId, storeId: storeId);
Active Store Context (active_store_provider.dart)
Manages currently selected store for bookstore owners.
class ActiveStoreId extends _$ActiveStoreId {
static const _storageKey = 'active_store_id';
String? build() {
_loadFromStorage();
return null;
}
Future<void> setActiveStore(String? storeId) async {
state = storeId;
final prefs = await SharedPreferences.getInstance();
if (storeId != null) {
await prefs.setString(_storageKey, storeId);
} else {
await prefs.remove(_storageKey);
}
}
}
Subscriptions (subscription_provider.dart)
Manages subscription tier and entitlements.
class SubscriptionTier extends _$SubscriptionTier {
Future<String> build() async {
final info = await SubscriptionService().getCustomerInfo();
return info.tier;
}
}
// Simple computed providers
bool isPremium(Ref ref) {
final tier = ref.watch(subscriptionTierProvider);
return tier.value == 'premium' || tier.value == 'bookstore';
}
Provider Patterns
Service Providers
Services are exposed as simple providers:
WishlistService wishlistService(Ref ref) {
return WishlistService(ApiClient());
}
Family Providers
Providers that accept parameters use the family pattern automatically:
class WishlistDetailNotifier extends _$WishlistDetailNotifier {
Future<Wishlist> build(String wishlistId) async {
// wishlistId parameter creates a family provider
}
}
// Usage
ref.watch(wishlistDetailNotifierProvider('wishlist-123'));
Auto-Refresh on Dependencies
Providers automatically rebuild when dependencies change:
Future<List<Book>> userBooks(Ref ref) async {
// Watches auth state
final authState = ref.watch(authNotifierProvider);
final userId = authState.value?.user?.id;
if (userId == null) return [];
// Rebuilds when auth changes
return bookService.getUserBooks(userId);
}
Manual Refresh
Providers can be manually refreshed:
// Refresh specific provider
ref.invalidate(wishlistsNotifierProvider);
// Refresh and wait for new data
await ref.refresh(wishlistsNotifierProvider.future);
State Updates
Optimistic Updates
Future<void> toggleLike(String postId) async {
// Optimistic update
state = state.whenData((posts) =>
posts.map((p) => p.id == postId ? p.copyWith(liked: !p.liked) : p).toList()
);
try {
await _service.toggleLike(postId);
} catch (e) {
// Revert on error
await refresh();
rethrow;
}
}
Loading States
Future<void> performAction() async {
state = const AsyncValue.loading();
state = await AsyncValue.guard(() async {
// Perform action
return newState;
});
}
Widget Integration
ConsumerWidget
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final data = ref.watch(myProvider);
return data.when(
data: (value) => Text(value),
loading: () => CircularProgressIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}
}
ConsumerStatefulWidget
class MyStatefulWidget extends ConsumerStatefulWidget {
ConsumerState<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends ConsumerState<MyStatefulWidget> {
Widget build(BuildContext context) {
final data = ref.watch(myProvider);
return Container();
}
}
Consumer Builder
// Use Consumer for local provider access
Consumer(
builder: (context, ref, child) {
final value = ref.watch(myProvider);
return Text(value);
},
)
Best Practices
- Use code generation - Avoid manual provider creation
- Keep providers focused - One provider per feature/entity
- Use family for parameters - Don't create provider factories
- Handle errors - Use AsyncValue's error state
- Invalidate on auth change - Clear cached data on logout
- Avoid nested providers - Keep dependency graph flat
- Use .notifier for mutations - Separate read and write
- Test providers - Use ProviderContainer for unit tests