State Management
DWallet uses Riverpod for state management, providing type-safe, testable, and reactive state handling throughout the app.
Why Riverpod?
Section titled “Why Riverpod?”Riverpod offers several advantages:
- Type-safe: Catch errors at compile time
- Testable: Easy to mock and test in isolation
- Reactive: Automatic UI updates when state changes
- Composable: Providers can depend on other providers
- DevTools: Excellent debugging support
Provider Types Used
Section titled “Provider Types Used”1. AsyncNotifier
Section titled “1. AsyncNotifier”For async operations like API calls or initialization that requires async work.
class AuthUserService extends AsyncNotifier<User> { @override Future<User> build() async { // Async initialization final prefs = await SharedPreferences.getInstance(); return _loadUser(prefs); }
Future<void> updateUser(User newUser) async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { await _saveUser(newUser); return newUser; }); }}Usage:
final userAsync = ref.watch(authUserServiceProvider);
userAsync.when( data: (user) => Text(user.fullName), loading: () => CircularProgressIndicator(), error: (err, stack) => Text('Error: $err'),);2. Notifier
Section titled “2. Notifier”For synchronous mutable state that needs methods to update it.
class ThemeService extends Notifier<ThemeModel> { @override ThemeModel build() { final storage = ref.read(sharedPrefsProvider); return _loadTheme(storage); }
void setThemeMode(ThemeMode mode) { state = state.copyWith(mode: mode); _persistTheme(); }
void setColorScheme(AppColorScheme scheme) { state = state.copyWith(colorScheme: scheme); _persistTheme(); }}Usage:
final theme = ref.watch(themeServiceProvider);
// In a button:ref.read(themeServiceProvider.notifier).setThemeMode(ThemeMode.dark);3. Provider
Section titled “3. Provider”For computed values or simple immutable state.
// Computed providerfinal userCurrencyProvider = Provider<Currency>((ref) { final userAsync = ref.watch(authUserServiceProvider); return userAsync.value?.currency ?? CurrencyMock.usd;});
// Simple providerfinal showLockScreenProvider = Provider<bool>((ref) { final securityState = ref.watch(securityProvider); return securityState.isAppLockEnabled && securityState.isLocked;});Provider Declaration Patterns
Section titled “Provider Declaration Patterns”Services
Section titled “Services”Services are typically declared as providers in the same file:
// In _theme_service.dartclass ThemeService extends Notifier<ThemeModel> { // ... implementation}
final themeServiceProvider = NotifierProvider<ThemeService, ThemeModel>( ThemeService.new,);Provider Dependencies
Section titled “Provider Dependencies”Providers can read other providers using ref.read() or ref.watch():
class SecurityNotifier extends Notifier<SecurityState> { @override SecurityState build() { // Read other providers in build() final storage = ref.read(securityStorageServiceProvider); return _loadState(storage); }
void someMethod() { // Read providers in methods final storage = ref.read(securityStorageServiceProvider); storage.savePin('1234'); }}State Immutability
Section titled “State Immutability”Always use immutable state objects with copyWith():
class SecurityState { final bool isLocked; final bool hasPin;
const SecurityState({ this.isLocked = false, this.hasPin = false, });
SecurityState copyWith({ bool? isLocked, bool? hasPin, }) { return SecurityState( isLocked: isLocked ?? this.isLocked, hasPin: hasPin ?? this.hasPin, ); }}Widget Integration
Section titled “Widget Integration”ConsumerWidget
Section titled “ConsumerWidget”For widgets that need to read providers:
class HomeView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themeServiceProvider);
return Scaffold( body: Text('Theme: ${theme.mode}'), ); }}ConsumerStatefulWidget
Section titled “ConsumerStatefulWidget”For widgets that need both state and providers:
class AppLockScreen extends ConsumerStatefulWidget { @override ConsumerState<AppLockScreen> createState() => _AppLockScreenState();}
class _AppLockScreenState extends ConsumerState<AppLockScreen> { @override Widget build(BuildContext context) { final lockoutState = ref.watch(lockoutProvider);
return Text('Attempts: ${lockoutState.failedAttempts}'); }}Selective Watching
Section titled “Selective Watching”Watch only specific properties to minimize rebuilds:
// Bad: Rebuilds when any theme property changesfinal theme = ref.watch(themeServiceProvider);Text(theme.mode.toString());
// Good: Only rebuilds when mode changesfinal mode = ref.watch( themeServiceProvider.select((t) => t.mode));Text(mode.toString());Testing Providers
Section titled “Testing Providers”void main() { test('SecurityNotifier verifies PIN correctly', () { final container = ProviderContainer();
final notifier = container.read(securityProvider.notifier); notifier.setPin('1234');
expect(notifier.verifyPin('1234'), true); expect(notifier.verifyPin('0000'), false); });}Best Practices
Section titled “Best Practices”- Use
ref.watch()in build methods for reactive UI updates - Use
ref.read()in callbacks (onPressed, initState, etc.) - Keep state immutable with
copyWith()pattern - Separate concerns - one provider per feature/area
- Use
selectfor granular rebuilds - Dispose resources in provider’s
dispose()method if needed