Structuring Flutter Apps with the BLoC Pattern

May 12, 2026 (3mo ago)

When a Flutter project grows past a handful of screens, the hardest part is no longer writing widgets. It’s keeping state changes predictable. The BLoC (Business Logic Component) pattern is the approach I keep coming back to, because it draws a hard line between what the UI shows and what the app does.

The core idea

A BLoC takes a stream of events in and emits a stream of states out. The widget layer never mutates data directly. It dispatches events and rebuilds when a new state arrives.

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
    on<Decrement>((event, emit) => emit(state - 1));
  }
}

The widget just listens:

BlocBuilder<CounterBloc, int>(
  builder: (context, count) => Text('$count'),
);

Conventions that make it scale

  • One BLoC per feature, not per screen. Screens come and go. The domain concept (cart, auth, profile) is what’s stable.
  • States are immutable and exhaustive. Model loading, success, and error as distinct states so the UI can’t represent an impossible combination.
  • Keep side effects out of build. Navigation and snackbars belong in a BlocListener, not inside BlocBuilder.

When not to reach for it

For a screen with a single toggle, a StatefulWidget is fine. BLoC earns its keep when state is shared, asynchronous, or needs to be tested in isolation.

That last point is the real win: because the logic lives in a plain Dart class, I can unit-test an entire feature without ever pumping a widget.