Advanced State Management (Bloc & Riverpod)

The BLoC Pattern: Core Concepts

16 min Lesson 2 of 14

The BLoC Pattern: Core Concepts

The Business Logic Component (BLoC) pattern is an architectural state management pattern developed by Google for Flutter. Its central philosophy is deceptively simple: Events go in, States come out. The BLoC sits between your UI layer and your data layer, receiving user-driven or system-driven events, processing business logic, and emitting new states that the UI reacts to. This creates a strict, testable boundary between presentation and logic.

Note: BLoC was introduced by Paolo Soares at Google I/O 2018 as a scalable pattern for Flutter. The official flutter_bloc package (by Felix Angelov) builds on top of the core concept and is the most widely adopted implementation in the community.

The Core Data Flow

Understanding BLoC starts with its unidirectional data flow. There are three key participants:

  • Event — an immutable object that represents something that happened (e.g., a button press, a page load, a search query submitted). Events are the inputs to a BLoC.
  • BLoC — the component that receives events and, after processing business logic (validation, API calls, transformations), emits new states.
  • State — an immutable object that describes the current condition of a feature (e.g., loading, loaded with data, error). States are the outputs of a BLoC.

The UI dispatches events to the BLoC and listens to state changes. Crucially, the UI never contains business logic, and the BLoC never holds a reference to UI widgets.

Defining Events and States

// --- Events (inputs to the BLoC) ---
abstract class CounterEvent {}

class CounterIncrementPressed extends CounterEvent {}
class CounterDecrementPressed extends CounterEvent {}
class CounterResetPressed extends CounterEvent {}

// --- States (outputs from the BLoC) ---
class CounterState {
  final int count;
  const CounterState(this.count);

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      (other is CounterState && other.count == count);

  @override
  int get hashCode => count.hashCode;
}

Implementing a BLoC

With the flutter_bloc package, a BLoC extends Bloc<Event, State>. You define an initial state in the constructor and register event handlers using on<EventType>(handler). Each handler receives the event and an Emitter that you call to push new states downstream.

A Complete Counter BLoC

import 'package:flutter_bloc/flutter_bloc.dart';

class CounterBloc extends Bloc<CounterEvent, CounterState> {
  CounterBloc() : super(const CounterState(0)) {
    on<CounterIncrementPressed>(_onIncrement);
    on<CounterDecrementPressed>(_onDecrement);
    on<CounterResetPressed>(_onReset);
  }

  void _onIncrement(
    CounterIncrementPressed event,
    Emitter<CounterState> emit,
  ) {
    emit(CounterState(state.count + 1));
  }

  void _onDecrement(
    CounterDecrementPressed event,
    Emitter<CounterState> emit,
  ) {
    // Business logic: prevent going below zero
    if (state.count > 0) {
      emit(CounterState(state.count - 1));
    }
  }

  void _onReset(
    CounterResetPressed event,
    Emitter<CounterState> emit,
  ) {
    emit(const CounterState(0));
  }
}
Tip: Keep event handler methods small and focused on a single responsibility. If a handler grows beyond 10–15 lines, consider extracting a private helper method or a dedicated service class. The BLoC should orchestrate logic, not implement it from scratch inline.

Connecting the BLoC to the UI

The flutter_bloc package provides two key widgets for connecting your BLoC to the widget tree:

  • BlocProvider — creates and provides the BLoC to its subtree via the widget tree (InheritedWidget under the hood). It also handles closing the BLoC when the widget is removed.
  • BlocBuilder — rebuilds its subtree whenever the BLoC emits a new state. It only rebuilds when the state actually changes (based on equality).

Wiring BLoC to the Widget Tree

// Provide the BLoC above the widget that needs it
BlocProvider(
  create: (context) => CounterBloc(),
  child: CounterPage(),
)

// Inside CounterPage — read and react to state
class CounterPage extends StatelessWidget {
  const CounterPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('BLoC Counter')),
      body: BlocBuilder<CounterBloc, CounterState>(
        builder: (context, state) {
          return Center(
            child: Text(
              'Count: ${state.count}',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
          );
        },
      ),
      floatingActionButton: Column(
        mainAxisAlignment: MainAxisAlignment.end,
        children: [
          FloatingActionButton(
            heroTag: 'inc',
            onPressed: () =>
                context.read<CounterBloc>().add(CounterIncrementPressed()),
            child: const Icon(Icons.add),
          ),
          const SizedBox(height: 8),
          FloatingActionButton(
            heroTag: 'dec',
            onPressed: () =>
                context.read<CounterBloc>().add(CounterDecrementPressed()),
            child: const Icon(Icons.remove),
          ),
        ],
      ),
    );
  }
}

Why BLoC Enforces a UI/Logic Boundary

The BLoC pattern enforces separation of concerns through its design constraints:

  • The BLoC receives only plain Dart objects (events) and emits only plain Dart objects (states). It has zero dependency on Flutter widgets or BuildContext.
  • Because the BLoC is pure Dart, it can be unit-tested without spinning up a widget tree. You feed events in and assert on the emitted states.
  • The UI layer is a thin shell — it maps user gestures to events and maps states to widgets. No if/else business logic should live in build() methods.
  • Multiple widgets can observe the same BLoC state independently, enabling fine-grained rebuilds without prop-drilling.
Warning: A common BLoC mistake is emitting the same state object consecutively. By default, flutter_bloc uses equality checks — if two successive states are equal, the BLoC will not emit the second one and BlocBuilder will not rebuild. Always ensure your state classes correctly implement == and hashCode, or use the equatable package.

Summary

The BLoC pattern structures state management around three concepts: Events (inputs), BLoC (processor), and States (outputs). Data flows in one direction — the UI dispatches events, the BLoC handles them and emits states, and the UI rebuilds in response. This separation makes large Flutter applications easier to reason about, test, and maintain. In the next lesson you will explore Cubit, a simplified variant of BLoC that replaces events with direct method calls.