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.

yaml
dependencies:
  flutter_bloc: ^8.1.6
  equatable: ^2.0.5   # value equality for states
  get_it: ^7.6.7      # service locator for DI

Modelling 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.

dart
// 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.

dart
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:

dart
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

Written by Iqbal Nova

Mobile Developer @ GMEDIA · Mobile Developer specializing in Flutter & React Native.