We use flutter_bloc as our state management solution. Bloc enforces a strict unidirectional data flow:
UI → Event → Bloc → State → UI
This separation ensures:
- Business logic is testable in isolation from the UI.
- State transitions are explicit and traceable.
- The UI is a pure function of the state.
| Class | Role |
|---|---|
Bloc<Event, State> |
Receives events, emits states |
BlocProvider |
Creates and provides a Bloc to the widget tree |
BlocBuilder |
Rebuilds UI in response to state changes |
BlocListener |
Reacts to state changes with side effects (navigation, snackbars) |
BlocConsumer |
Combines BlocBuilder + BlocListener |
MultiBlocProvider |
Provides multiple Blocs at once |
MultiBlocListener |
Listens to multiple Blocs at once |
context.read<T>() |
Access a Bloc without subscribing |
context.watch<T>() |
Access a Bloc and subscribe to rebuilds |
Blocs are created and provided by Page widgets. They are consumed by View and Body widgets. See Pages and Blocs for full rules.
Blocs may only depend on use cases — never on repository interfaces or infrastructure directly. This ensures the Bloc stays decoupled from data-fetching details.
// ✅
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
ProfileBloc({required GetUserUseCase getUserUseCase})
: _getUser = getUserUseCase, ...
// ❌
class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
ProfileBloc({required IUserRepository userRepository})
: _userRepository = userRepository, ...Blocs must not directly communicate with or depend on other Blocs. Inter-Bloc communication must happen externally, via BlocListener in the widget tree or by using context.read<OtherBloc>().add(...) inside a listener.
// ✅ — Cross-Bloc communication via BlocListener
BlocListener<AuthBloc, AuthState>(
listenWhen: (prev, curr) => curr.status == .authenticated,
listener: (context, state) {
context.read<ProfileBloc>().add(const ProfileLoadRequested());
},
child: ...,
)Use case methods return TaskEither<E, T>. Always await the .run() call and use .fold to emit success or failure states:
Future<void> _onSignInRequested(
SignInRequested event,
Emitter<LoginState> emit,
) async {
emit(state.copyWith(status: .loading));
final result = await _signIn.execute(
email: event.email,
password: event.password,
).run();
result.fold(
(error) => emit(state.copyWith(status: .failure, error: error)),
(user) => emit(state.copyWith(status: .success, user: user)),
);
}