Blog
12

BLoC State Not Rebuilding: The Equatable Trap That Swallows Your Emissions

Flutter BLoC State Not Rebuilding? The Equatable Trap Explained

March 26, 2026

No error message. No crash. No red screen. The app just ignores you.

You changed the data. You called emit. You verified the event fired. You added print statements and confirmed the new state was constructed with the right values. And the UI sits there, unchanged, like nothing happened.

This is the most frustrating category of bug in Flutter — the kind with no error to Google. The app doesn't fail. It just silently does the wrong thing. And in BLoC-based architectures, the most common cause is a broken equality check that tells BLoC the state hasn't changed — when it has.

How BLoC Decides Whether to Rebuild

BLoC has a simple rule: if the new state equals the previous state, don't emit it. This is an optimization. If a BlocBuilder receives the same state twice, there's no reason to rebuild the widget tree. The framework skips the emission entirely.

The check happens inside emit:

dart
// Simplified from the bloc package source:
void emit(State state) {
  if (state == _state) return; // ← this line
  _state = state;
  _controller.add(state);
}

That == comparison is where everything goes right or wrong. It relies entirely on how your state class implements equality.

Equatable: What It Does and Why You Use It

Dart objects use reference equality by default. Two different instances of the same class with the same fields are not equal:

dart
class SearchState {
  final String query;
  final List<Lead> results;

  SearchState({required this.query, required this.results});
}

final a = SearchState(query: 'flutter', results: leads);
final b = SearchState(query: 'flutter', results: leads);

print(a == b); // false — different instances

This means BLoC would emit every state, even identical ones. Your UI rebuilds when nothing changed. That's wasteful but not broken — you'd never notice unless you profile.

Equatable solves this by overriding == to compare specific fields you define in a props list:

dart
class SearchState extends Equatable {
  final String query;
  final List<Lead> results;

  const SearchState({required this.query, required this.results});

  @override
  List<Object?> get props => [query, results];
}

final a = SearchState(query: 'flutter', results: leads);
final b = SearchState(query: 'flutter', results: leads);

print(a == b); // true — same query, same results list reference

Now BLoC can skip duplicate emissions. Efficient. Clean. Until you forget to update props.

The Trap: Adding a Field, Forgetting `props`

Here's where it breaks. Your SearchState works fine with query and results. Then you add a loading flag:

dart
class SearchState extends Equatable {
  final String query;
  final List<Lead> results;
  final bool isLoading; // new field

  const SearchState({
    required this.query,
    required this.results,
    this.isLoading = false,
  });

  @override
  List<Object?> get props => [query, results]; // ← forgot isLoading
}

Now watch what happens:

dart
// In your BLoC:
void _onSearch(SearchEvent event, Emitter<SearchState> emit) async {
  emit(SearchState(query: event.query, results: state.results, isLoading: true));

  final results = await searchUseCase(event.query);

  emit(SearchState(query: event.query, results: results, isLoading: false));
}

The first emit sets isLoading: true. But props doesn't include isLoading. If query and results haven't changed yet, Equatable says the states are equal. BLoC swallows the emission. Your loading spinner never appears.

The second emit updates results, which is in props, so it goes through. The UI jumps directly from idle to showing results — no loading state in between.

No error. No warning. The code looks correct. You just never see the loading indicator, and you have no idea why.

The Fix

dart
@override
List<Object?> get props => [query, results, isLoading];

One field added to one list. That's it. But finding this without knowing where to look can cost an hour of print-statement debugging.

The Other Direction: Mutating Instead of Replacing

There's a mirror-image version of this bug that's equally silent. Instead of forgetting a field in props, you mutate an existing one:

dart
void _onLeadAdded(LeadAdded event, Emitter<SearchState> emit) {
  state.results.add(event.lead); // mutating the existing list
  emit(SearchState(
    query: state.query,
    results: state.results, // same list reference
    isLoading: false,
  ));
}

state.results is the same List object. You added an item to it, but the reference didn't change. Equatable compares the reference (via == on the list), sees the same object, and says "equal." BLoC swallows the emission.

The fix:

dart
void _onLeadAdded(LeadAdded event, Emitter<SearchState> emit) {
  final updatedResults = [...state.results, event.lead]; // new list
  emit(SearchState(
    query: state.query,
    results: updatedResults,
    isLoading: false,
  ));
}

A new list with the same contents plus the new item. Different reference. Equatable sees a change. BLoC emits. The UI rebuilds.

This is immutability in practice — not as a theoretical ideal, but as the thing that makes your state management actually work.

Why This Is a Clean Architecture Problem

In clean architecture, your state class sits at the boundary between the presentation layer and the rest of the system. It's the contract between your BLoC and your UI. When that contract is broken — when the state says "I haven't changed" while carrying different data — the UI trusts it and does nothing.

The use case returned the right data. The repository fetched correctly. The BLoC event handler ran. Everything below the surface worked perfectly. The failure happens at the very last mile, in the state class's equality definition, which is technically presentation-layer code.

This makes it hard to debug because you instinctively look deeper. You check the API response. You verify the repository mapping. You add logs to the use case. Everything looks correct, because everything is correct — except the one class whose entire job is to carry data from the BLoC to the widget.

Debugging Checklist

When your BLoC emits but the UI doesn't rebuild:

  1. Check `props` — does it include every field? This is the cause 80% of the time.
  2. Check for mutation — are you modifying a list or map in place instead of creating a new one? Use [...list] or {...map} to create copies.
  3. Check `buildWhen` — if your BlocBuilder has a buildWhen callback, it might be filtering out the emission you care about.
  4. Check the widget tree — is the BlocBuilder actually above the widget that needs to rebuild? A BlocBuilder at the wrong level of the tree won't help.
  5. Temporarily remove Equatable — if the UI suddenly works without it, you've confirmed the equality check is the problem. Now find which field is missing from props.

Key Insights

Equatable is a sharp tool. It solves a real problem — preventing unnecessary rebuilds — but it requires you to maintain props every time you touch the state class. One forgotten field, and BLoC silently drops emissions. The convenience of value equality comes with the responsibility of keeping the equality definition complete.

Immutability isn't optional with BLoC. Mutating a list inside a state object and re-emitting it will always fail with Equatable, because the reference hasn't changed. Every state emission must carry new object references for any field that changed. This is the price of the == optimization — your state must be truly new, not a modified version of the old one.

No error is worse than a wrong error. A crash tells you something broke. A silent non-emission tells you nothing. If you're choosing between "too many rebuilds" (no Equatable) and "risk missing emissions" (with Equatable), err on the side of rebuilding too much until your state classes are stable. You can optimize later. You can't debug what you can't see.

Consider `Freezed` for critical states. The Freezed package generates immutable classes with correct ==, hashCode, and copyWith automatically. You never write props by hand, so you never forget a field. The tradeoff is code generation overhead — but for state classes that change often, it removes an entire category of bugs.

Related Topics

flutter bloc not updating uibloc state not rebuildingequatable props missing fieldbloc emit not workingflutter equatable trapbloc duplicate stateflutter bloc ui not refreshingbloc swallows emissionequatable list mutationflutter bloc buildWhen

Ready to build your app?

Flutter apps built on Clean Architecture — documented, tested, and yours to own. See which plan fits your project.