# State Management > State management patterns in Flutter app. --- ## Overview This app uses **ChangeNotifier + Provider** for state management: - **AuthBloc** uses `ChangeNotifier` for auth state - **State classes** are immutable value objects - **Repository pattern** separates data access from business logic - **DI** via provider/factory pattern --- ## State Management Pattern ### Bloc/Cubit Pattern **Use `ChangeNotifier` for complex state:** ```dart // features/auth/presentation/bloc/auth_bloc.dart class AuthBloc extends ChangeNotifier { AuthBloc({required AuthRepository repository}) : _repository = repository; final AuthRepository _repository; final Logger _logger = getLogger('features.auth.bloc'); AuthState _state = AuthState.initial; AuthState get state => _state; Future loginWithOtp({ required String email, required String otp, }) async { final user = await _repository.loginWithEmailOtp(email: email, otp: otp); _logger.info(message: 'User logged in', extra: {'user_id': user.id}); _state = AuthState(status: AuthStatus.authenticated, user: user); notifyListeners(); } } ``` ### Immutable State **State classes should be immutable:** ```dart // features/auth/presentation/bloc/auth_state.dart class AuthState { const AuthState({ required this.status, this.user, this.errorMessage, }); final AuthStatus status; final AuthUser? user; final String? errorMessage; factory AuthState.initial() => const AuthState(status: AuthStatus.initial); AuthState copyWith({ AuthStatus? status, AuthUser? user, String? errorMessage, }) { return AuthState( status: status ?? this.status, user: user ?? this.user, errorMessage: errorMessage ?? this.errorMessage, ); } } enum AuthStatus { initial, loading, authenticated, unauthenticated, } ``` --- ## Repository Pattern ### Repository Interface **Define interface in presentation layer:** ```dart // features/auth/data/repositories/auth_repository.dart abstract class AuthRepository { Future recoverSession(); Future sendOtp(String email); Future loginWithEmailOtp({ required String email, required String otp, }); Future logout(); Future clearLocalSession(); } ``` ### Repository Implementation **Implement with data sources:** ```dart class AuthRepositoryImpl implements AuthRepository { AuthRepositoryImpl({ required AuthApi api, required SessionStore sessionStore, }) : _api = api, _sessionStore = sessionStore; final AuthApi _api; final SessionStore _sessionStore; @override Future recoverSession() async { final session = await _sessionStore.load(); if (session == null) return null; try { return await _api.getCurrentUser(session.accessToken); } catch (e) { await clearLocalSession(); return null; } } } ``` --- ## Error Handling in State ### Try-Catch with Logging **Every async operation should handle errors:** ```dart Future start() async { _state = _state.copyWith(status: AuthStatus.loading); notifyListeners(); try { final user = await _repository.recoverSession(); if (user == null) { _state = const AuthState(status: AuthStatus.unauthenticated); } else { _state = AuthState(status: AuthStatus.authenticated, user: user); } notifyListeners(); } catch (error, stackTrace) { _logger.error( message: 'Session recovery failed: ${error.runtimeType}', error: error, stackTrace: stackTrace, ); await _repository.clearLocalSession(); _state = AuthState( status: AuthStatus.unauthenticated, errorMessage: _toSafeMessage(error), ); notifyListeners(); } } ``` ### Global Error Handling **401 Session Invalidation:** ```dart // Global callback for 401 errors Future handleUnauthorized401() async { if (_handlingUnauthorized) return; _handlingUnauthorized = true; try { await _repository.clearLocalSession(); _logger.warning(message: 'Session invalidated by 401 callback'); _state = const AuthState(status: AuthStatus.unauthenticated); notifyListeners(); } finally { _handlingUnauthorized = false; } } ``` --- ## DI Pattern ### Factory Registration **Register repositories and blocs:** ```dart // app/di/di.dart class ServiceLocator { static late AuthRepository authRepository; static late AuthBloc authBloc; static void setup() { authRepository = AuthRepositoryImpl( api: AuthApiImpl(), sessionStore: SessionStore(), ); authBloc = AuthBloc(repository: authRepository); } } ``` ### Widget Access **Access via Provider or global:** ```dart // Using global reference final authBloc = ServiceLocator.authBloc; // In widget class LoginScreen extends StatelessWidget { @override Widget build(BuildContext context) { return ListenableBuilder( listenable: authBloc, builder: (context, child) { if (authBloc.state.status == AuthStatus.loading) { return AppLoadingIndicator(); } return LoginForm(); }, ); } } ``` --- ## Common Mistakes ### ❌ Mutable State ```dart // WRONG: Mutating state directly class AuthBloc extends ChangeNotifier { AuthUser? user; void login() { user = fetchUser(); // Direct mutation notifyListeners(); } } ``` **Right: Immutable state with copyWith:** ```dart class AuthBloc extends ChangeNotifier { AuthState _state = AuthState.initial; AuthState get state => _state; void login() async { final user = await fetchUser(); _state = _state.copyWith(user: user, status: AuthStatus.authenticated); notifyListeners(); } } ``` ### ❌ Importing Feature Data Layer from Other Features ```dart // WRONG: Direct import from another feature import 'package:app/features/auth/data/repositories/auth_repository.dart'; ``` **Right: Access via app-level facade or DI:** ```dart final authRepository = ServiceLocator.authRepository; ``` ### ❌ Skipping Error Logging ```dart // WRONG: No logging try { await repository.operation(); } catch (e) { state = AuthState(status: AuthStatus.error); } ``` **Right: Log before state change:** ```dart try { await repository.operation(); } catch (e, stackTrace) { _logger.error( message: 'Operation failed', error: e, stackTrace: stackTrace, ); state = AuthState(status: AuthStatus.error); } ``` ### ❌ Creating Per-Widget State Instances ```dart // WRONG: New instance per widget class MyWidget extends StatelessWidget { final authBloc = AuthBloc(); // New instance every build } ``` **Right: Use DI singleton:** ```dart class MyWidget extends StatelessWidget { @override Widget build(BuildContext context) { final authBloc = ServiceLocator.authBloc; // Singleton // ... } } ``` --- ## Module Naming Convention **Logger module path:** | Feature | Module Path | |---------|------------| | auth | `features.auth` | | home | `features.home` | | divination | `features.divination` | | settings | `features.settings` | ```dart class AuthBloc extends ChangeNotifier { final Logger _logger = getLogger('features.auth.bloc'); // ... } ``` --- ## Testing State ### Unit Testing Bloc ```dart test('AuthBloc login success', () async { final mockRepo = MockAuthRepository(); final bloc = AuthBloc(repository: mockRepo); when(mockRepo.loginWithEmailOtp( email: 'test@example.com', otp: '123456', )).thenAnswer((_) async => AuthUser(id: '123', email: 'test@example.com')); await bloc.loginWithOtp(email: 'test@example.com', otp: '123456'); expect(bloc.state.status, AuthStatus.authenticated); expect(bloc.state.user?.id, '123'); }); ``` ### Integration Testing ```dart testWidgets('Login screen shows error on failed login', (tester) async { await tester.pumpWidget(MyApp()); await tester.enterText(find.byKey(Key('email_field')), 'test@example.com'); await tester.enterText(find.byKey(Key('otp_field')), 'wrong'); await tester.tap(find.byKey(Key('login_button'))); await tester.pumpAndSettle(); expect(find.text('Request failed, please try again'), findsOneWidget); }); ```