Skip to main content

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

  1. Use code generation - Avoid manual provider creation
  2. Keep providers focused - One provider per feature/entity
  3. Use family for parameters - Don't create provider factories
  4. Handle errors - Use AsyncValue's error state
  5. Invalidate on auth change - Clear cached data on logout
  6. Avoid nested providers - Keep dependency graph flat
  7. Use .notifier for mutations - Separate read and write
  8. Test providers - Use ProviderContainer for unit tests