Lesson 33 of 51 intermediate

BLoC Pattern - Events & States

The traffic controller for your app's brain

Open interactive version (quiz + challenge)

Real-world analogy

Imagine a restaurant kitchen. Customers send in orders (Events) to the kitchen. The chef (BLoC) reads each order and prepares dishes. When a dish is ready, it goes out on a tray (State) to the dining room (UI). The chef never walks into the dining room, and the customers never walk into the kitchen. Events flow in, States flow out. That clean separation is what makes a restaurant -- and a BLoC -- work smoothly.

What is it?

The BLoC (Business Logic Component) pattern is an architecture for Flutter apps that separates business logic from the UI using events and states. You define Event classes that represent user actions or system triggers, and State classes that represent the current condition of a feature. The BLoC receives events, processes them through business logic, and emits new states that the UI reacts to. Combined with Equatable for efficient comparison, this creates a predictable, testable, and scalable architecture.

Real-world relevance

BLoC is one of the most widely adopted state management solutions in production Flutter apps, used by companies like BMW, Alibaba, and Google teams. The event-state pattern maps naturally to how users interact with apps: the user does something (event), the app processes it (BLoC logic), and the screen updates (state). In team_mvp_kit, every feature follows this pattern with BaseBloc, BaseEvent, and BaseState, making the codebase consistent and onboarding new developers straightforward.

Key points

Code example

import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// ---- Base classes (from team_mvp_kit) ----

abstract class BaseEvent extends Equatable {
  const BaseEvent();
  @override
  List<Object?> get props => [];
}

abstract class BaseState extends Equatable {
  const BaseState();
  @override
  List<Object?> get props => [];
}

// ---- Events ----

class LoadTodos extends BaseEvent {}

class AddTodo extends BaseEvent {
  final String title;
  const AddTodo(this.title);
  @override
  List<Object?> get props => [title];
}

class ToggleTodo extends BaseEvent {
  final int index;
  const ToggleTodo(this.index);
  @override
  List<Object?> get props => [index];
}

class DeleteTodo extends BaseEvent {
  final int index;
  const DeleteTodo(this.index);
  @override
  List<Object?> get props => [index];
}

// ---- State ----

class TodoItem {
  final String title;
  final bool isDone;
  const TodoItem({required this.title, this.isDone = false});

  TodoItem copyWith({String? title, bool? isDone}) {
    return TodoItem(
      title: title ?? this.title,
      isDone: isDone ?? this.isDone,
    );
  }
}

class TodoState extends BaseState {
  final List<TodoItem> todos;
  final bool isLoading;
  final String? error;

  const TodoState({
    this.todos = const [],
    this.isLoading = false,
    this.error,
  });

  TodoState copyWith({
    List<TodoItem>? todos,
    bool? isLoading,
    String? error,
  }) {
    return TodoState(
      todos: todos ?? this.todos,
      isLoading: isLoading ?? this.isLoading,
      error: error,
    );
  }

  @override
  List<Object?> get props => [todos, isLoading, error];
}

// ---- BLoC ----

class TodoBloc extends Bloc<BaseEvent, TodoState> {
  TodoBloc() : super(const TodoState()) {
    on<LoadTodos>(_onLoad);
    on<AddTodo>(_onAdd);
    on<ToggleTodo>(_onToggle);
    on<DeleteTodo>(_onDelete);
  }

  Future<void> _onLoad(
    LoadTodos event,
    Emitter<TodoState> emit,
  ) async {
    emit(state.copyWith(isLoading: true, error: null));
    try {
      await Future.delayed(const Duration(seconds: 1));
      final todos = [
        const TodoItem(title: 'Learn Dart'),
        const TodoItem(title: 'Master BLoC'),
        const TodoItem(title: 'Build an app'),
      ];
      emit(state.copyWith(todos: todos, isLoading: false));
    } catch (e) {
      emit(state.copyWith(
        isLoading: false,
        error: e.toString(),
      ));
    }
  }

  void _onAdd(AddTodo event, Emitter<TodoState> emit) {
    final updated = [
      ...state.todos,
      TodoItem(title: event.title),
    ];
    emit(state.copyWith(todos: updated));
  }

  void _onToggle(ToggleTodo event, Emitter<TodoState> emit) {
    final updated = state.todos.asMap().entries.map((entry) {
      if (entry.key == event.index) {
        return entry.value.copyWith(isDone: !entry.value.isDone);
      }
      return entry.value;
    }).toList();
    emit(state.copyWith(todos: updated));
  }

  void _onDelete(DeleteTodo event, Emitter<TodoState> emit) {
    final updated = [...state.todos]..removeAt(event.index);
    emit(state.copyWith(todos: updated));
  }
}

Line-by-line walkthrough

  1. 1. Define BaseEvent extending Equatable with an empty props list as the parent of all events.
  2. 2. Define BaseState extending Equatable as the parent of all state classes.
  3. 3. LoadTodos event has no data -- it just signals the BLoC to fetch todos.
  4. 4. AddTodo carries a title string and includes it in props for equality.
  5. 5. ToggleTodo and DeleteTodo carry an index to identify which todo to act on.
  6. 6. TodoItem is a simple data class with title and isDone, plus a copyWith method.
  7. 7. TodoState holds a list of TodoItems, a loading flag, and an optional error string.
  8. 8. The copyWith method creates a new TodoState, only replacing the fields you specify.
  9. 9. The props getter lists all three fields so Equatable can compare states accurately.
  10. 10. TodoBloc extends Bloc with BaseEvent input and TodoState output, starting with an empty state.
  11. 11. Register four event handlers in the constructor using the on syntax.
  12. 12. _onLoad emits loading state, simulates an async fetch, then emits loaded state with todos.
  13. 13. The try-catch ensures that API failures emit an error state instead of crashing.
  14. 14. _onAdd spreads existing todos and appends a new TodoItem, then emits the updated state.
  15. 15. _onToggle maps over todos, flipping isDone only for the matching index.
  16. 16. _onDelete copies the list, removes the item at the given index, and emits.

Spot the bug

class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>(_onIncrement);
    on<Decrement>(_onDecrement);
  }

  void _onIncrement(Increment event, Emitter<int> emit) {
    state++;
    emit(state);
  }

  void _onDecrement(Decrement event, Emitter<int> emit) {
    emit(state - 1);
  }
}
Need a hint?
BLoC state is immutable. Can you modify it directly?
Show answer
In _onIncrement, 'state++' tries to mutate the state directly, which is not allowed. The state property in a Bloc is read-only. The fix is to emit a new value: 'emit(state + 1)' instead of 'state++; emit(state)'. The _onDecrement handler is correct because it creates a new value with 'state - 1'.

Explain like I'm 5

Imagine you are ordering food at a drive-through. You talk into the speaker and say what you want -- that is an Event, like 'I want a burger.' The kitchen hears your order, cooks the food, and hands it out the window -- that is the State, like 'Here is your burger, ready to eat.' You never go into the kitchen, and the cook never comes to your car. The speaker and window keep everything organized. BLoC works the same way: your app screen shouts events into a speaker, and the BLoC kitchen sends back states through a window!

Fun fact

The BLoC pattern was originally created by Paolo Soares and Cong Hui from Google and was first presented at DartConf 2018. It was designed to allow code sharing between Flutter and AngularDart apps, which is why it uses pure Dart streams with no Flutter dependency in the core logic.

Hands-on challenge

Create a complete ShoppingCartBloc with events (AddItem, RemoveItem, ClearCart, ApplyDiscount) and a single CartState class with fields for items list, total price, and optional discount percentage. Use BaseEvent and BaseState from team_mvp_kit patterns. Implement the Bloc with all event handlers. Make sure your state has a proper copyWith method and Equatable props.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter & Dart