BLoC (Business Logic Component) is the state management pattern I use in every Flutter production app. When combined with Clean Architecture, it creates codebases that are testable, scalable, and maintainable across a team. Here's the practical setup I use — no theoretical fluff.
The Three Layers
Clean Architecture in Flutter splits code into three concentric layers, each owning a clear responsibility:
- Data Layer: repositories (implementations), data sources (API/local DB), DTOs
- Domain Layer: repository interfaces, use cases (interactors), domain entities
- Presentation Layer: BLoC (events + states + logic), UI widgets
Setting Up flutter_bloc
Add the dependency to pubspec.yaml. The flutter_bloc package provides BlocProvider, BlocBuilder, BlocListener and the base Bloc/Cubit classes.
dependencies:
flutter_bloc: ^8.1.6
equatable: ^2.0.5 # value equality for states
get_it: ^7.6.7 # service locator for DIModelling Events and States
The key insight in BLoC is separating intent (events) from outcome (states). Use Equatable for states so BlocBuilder only rebuilds when state actually changes.
// Events — user intents
abstract class MovieEvent extends Equatable {}
class FetchMovies extends MovieEvent {
final String query;
const FetchMovies(this.query);
@override List<Object> get props => [query];
}
// States — outcomes
abstract class MovieState extends Equatable {}
class MovieInitial extends MovieState { @override List<Object> get props => []; }
class MovieLoading extends MovieState { @override List<Object> get props => []; }
class MovieLoaded extends MovieState {
final List<Movie> movies;
const MovieLoaded(this.movies);
@override List<Object> get props => [movies];
}
class MovieError extends MovieState {
final String message;
const MovieError(this.message);
@override List<Object> get props => [message];
}The BLoC Class
The Bloc class maps events to states via async* emit streams. Each event handler calls a use case, never touching the data layer directly.
class MovieBloc extends Bloc<MovieEvent, MovieState> {
final FetchMoviesUseCase _fetchMovies;
MovieBloc(this._fetchMovies) : super(MovieInitial()) {
on<FetchMovies>(_onFetchMovies);
}
Future<void> _onFetchMovies(
FetchMovies event, Emitter<MovieState> emit) async {
emit(MovieLoading());
final result = await _fetchMovies(event.query);
result.fold(
(failure) => emit(MovieError(failure.message)),
(movies) => emit(MovieLoaded(movies)),
);
}
}Testing is the Payoff
The real reward of this pattern is testability. Because BLoC depends on interfaces (use cases), you can mock the entire data layer in unit tests. Use bloc_test for expressive Bloc unit tests:
blocTest<MovieBloc, MovieState>(
'emits [Loading, Loaded] when FetchMovies succeeds',
build: () => MovieBloc(mockFetchMovies),
act: (bloc) => bloc.add(FetchMovies('batman')),
expect: () => [MovieLoading(), MovieLoaded(tMovies)],
);Common Mistakes to Avoid
After using BLoC in production, these are the traps I see most often:
- Putting business logic in widgets — always delegate to BLoC via events
- Creating a single BLoC for an entire screen — split by feature/concern
- Forgetting to close streams — always call bloc.close() in dispose()
- Using Cubit when you need auditability — Bloc gives you full event history for debugging