State Management
Understanding the state management architecture in Flutter Server Box.
Why Riverpod?
Section titled “Why Riverpod?”Key Benefits:
- Compile-time safety: Catch errors at compile time
- No BuildContext needed: Access state anywhere
- Easy testing: Simple to test providers in isolation
- Code generation: Less boilerplate, type-safe
Provider Architecture
Section titled “Provider Architecture”┌─────────────────────────────────────────────┐│ UI Layer (Widgets) ││ - ConsumerWidget / ConsumerStatefulWidget ││ - ref.watch() / ref.read() │└─────────────────────────────────────────────┘ ↓ watches┌─────────────────────────────────────────────┐│ Provider Layer ││ - @riverpod annotations ││ - Generated *.g.dart files │└─────────────────────────────────────────────┘ ↓ uses┌─────────────────────────────────────────────┐│ Service / Store Layer ││ - Business logic ││ - Data access │└─────────────────────────────────────────────┘Provider Types Used
Section titled “Provider Types Used”1. StateProvider (Simple State)
Section titled “1. StateProvider (Simple State)”For simple, observable state:
@riverpodclass ThemeNotifier extends _$ThemeNotifier { @override ThemeMode build() { // Load from settings return SettingStore.themeMode; }
void setTheme(ThemeMode mode) { state = mode; SettingStore.themeMode = mode; // Persist }}Usage:
class MyWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themeNotifierProvider); return Text('Theme: $theme'); }}2. AsyncNotifierProvider (Async State)
Section titled “2. AsyncNotifierProvider (Async State)”For data that loads asynchronously:
@riverpodclass ServerStatus extends _$ServerStatus { @override Future<StatusModel> build(Server server) async { // Initial load return await fetchStatus(server); }
Future<void> refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() async { return await fetchStatus(server); }); }}Usage:
final status = ref.watch(serverStatusProvider(server));
status.when( data: (data) => StatusWidget(data), loading: () => LoadingWidget(), error: (error, stack) => ErrorWidget(error),)3. StreamProvider (Real-time Data)
Section titled “3. StreamProvider (Real-time Data)”For continuous data streams:
@riverpodStream<CpuUsage> cpuUsage(CpuUsageRef ref, Server server) { final client = ref.watch(sshClientProvider(server)); final stream = client.monitorCpu();
// Auto-dispose when not watched ref.onDispose(() { client.stopMonitoring(); });
return stream;}Usage:
final cpu = ref.watch(cpuUsageProvider(server));
cpu.when( data: (usage) => CpuChart(usage), loading: () => CircularProgressIndicator(), error: (error, stack) => ErrorWidget(error),)4. Family Providers (Parameterized)
Section titled “4. Family Providers (Parameterized)”Providers that accept parameters:
@riverpodFuture<List<Container>> containers(ContainersRef ref, Server server) async { final client = await ref.watch(sshClientProvider(server).future); return await client.listContainers();}Usage:
final containers = ref.watch(containersProvider(server));
// Different servers = different cached statesfinal containers2 = ref.watch(containersProvider(server2));State Update Patterns
Section titled “State Update Patterns”Direct State Update
Section titled “Direct State Update”ref.read(settingsProvider.notifier).updateTheme(darkMode);Computed State
Section titled “Computed State”@riverpodint totalServers(TotalServersRef ref) { final servers = ref.watch(serversProvider); return servers.length;}Derived State
Section titled “Derived State”@riverpodList<Server> onlineServers(OnlineServersRef ref) { final all = ref.watch(serversProvider); return all.where((s) => s.isOnline).toList();}Server-Specific State
Section titled “Server-Specific State”Per-Server Providers
Section titled “Per-Server Providers”Each server has isolated state:
@riverpodclass ServerProvider extends _$ServerProvider { @override ServerState build(Server server) { return ServerState.disconnected(); }
Future<void> connect() async { state = ServerState.connecting(); try { final client = await genClient(server.spi); state = ServerState.connected(client); } catch (e) { state = ServerState.error(e.toString()); } }}Provider Keys
Section titled “Provider Keys”// Unique provider per server@riverpodServerStatus serverStatus(ServerStatusRef ref, Server server) { // server.id used as key}Reactive Patterns
Section titled “Reactive Patterns”Auto-Refresh
Section titled “Auto-Refresh”@riverpodclass AutoRefreshServerStatus extends _$AutoRefreshServerStatus { Timer? _timer;
@override Future<StatusModel> build(Server server) async { // Start timer _timer = Timer.periodic(Duration(seconds: 5), (_) { refresh(); });
ref.onDispose(() { _timer?.cancel(); });
return await fetchStatus(server); }
Future<void> refresh() async { state = const AsyncValue.loading(); state = await AsyncValue.guard(() => fetchStatus(server)); }}Multi-Provider Dependencies
Section titled “Multi-Provider Dependencies”@riverpodFuture<SystemInfo> systemInfo(SystemInfoRef ref, Server server) async { // Wait for SSH client first final client = await ref.watch(sshClientProvider(server).future);
// Then fetch system info return await client.getSystemInfo();}State Persistence
Section titled “State Persistence”Hive Integration
Section titled “Hive Integration”@riverpodclass ServerStoreNotifier extends _$ServerStoreNotifier { @override List<Server> build() { // Load from Hive return Hive.box<Server>('servers').values.toList(); }
void addServer(Server server) { state = [...state, server]; // Persist to Hive Hive.box<Server>('servers').put(server.id, server); }
void removeServer(String id) { state = state.where((s) => s.id != id).toList(); // Remove from Hive Hive.box<Server>('servers').delete(id); }}Error Handling
Section titled “Error Handling”Error States
Section titled “Error States”@riverpodclass ConnectionManager extends _$ConnectionManager { @override ConnectionState build() { return ConnectionState.idle(); }
Future<void> connect(Server server) async { state = ConnectionState.connecting(); try { final client = await genClient(server.spi); state = ConnectionState.connected(client); } on SocketException catch (e) { state = ConnectionState.error('Network error: $e'); } on AuthenticationException catch (e) { state = ConnectionState.error('Auth failed: $e'); } catch (e) { state = ConnectionState.error('Unknown error: $e'); } }}Error Recovery
Section titled “Error Recovery”@riverpodclass ResilientFetcher extends _$ResilientFetcher { int _retryCount = 0;
@override Future<Data> build(Server server) async { return await _fetchWithRetry(); }
Future<Data> _fetchWithRetry() async { try { return await fetchData(server); } catch (e) { if (_retryCount < 3) { _retryCount++; await Future.delayed(Duration(seconds: 2)); return await _fetchWithRetry(); } rethrow; } }}Performance Optimizations
Section titled “Performance Optimizations”Provider Keep-Alive
Section titled “Provider Keep-Alive”@Riverpod(keepAlive: true) // Don't dispose when no listenersclass GlobalSettings extends _$GlobalSettings { @override Settings build() { return Settings.defaults(); }}Selective Watching
Section titled “Selective Watching”// Watch only specific part of statefinal name = ref.watch(serverProvider.select((s) => s.name));Provider Caching
Section titled “Provider Caching”Family providers cache results per parameter:
// Cached per server IDfinal status1 = ref.watch(serverStatusProvider(server1));final status2 = ref.watch(serverStatusProvider(server2));// Different states, both cachedTesting with Riverpod
Section titled “Testing with Riverpod”Provider Container
Section titled “Provider Container”test('fetch server status', () async { final container = ProviderContainer(); addTearDown(container.dispose);
// Override provider container.overrideFactory( sshClientProvider, (ref, server) => MockSshClient(), );
final status = await container.read( serverStatusProvider(testServer).future, );
expect(status, isA<StatusModel>());});Best Practices
Section titled “Best Practices”- Co-locate providers: Place near consuming widgets
- Use code generation: Always use
@riverpod - Keep providers focused: Single responsibility
- Handle loading states: Always handle AsyncValue states
- Dispose resources: Use
ref.onDispose()for cleanup - Avoid deep provider trees: Keep provider graph flat