Flutter Bloc State Not Emitting When Value Is Unchanged: Fixes

July 12, 2026 8 min read

You emit a state from your Bloc, the method returns without error, but the widget on screen never rebuilds. The logic ran β€” you can see it in the debug console β€” but the UI is frozen. This is one of the most frustrating silent failures in Flutter development, and the cause is almost always the same: Bloc compared your new state to the old one, found them equal, and quietly did nothing.

What You'll Learn

  • Why Bloc performs equality checks before emitting and how to inspect that behavior
  • How Equatable interacts with Bloc's emission pipeline in ways that surprise developers
  • Five concrete fixes you can apply depending on your state model
  • The pitfalls that introduce subtle bugs when you try to work around equality checks

How Bloc Decides Whether to Emit a State

Bloc (and its sibling Cubit) uses a simple rule before every emit() call: if the new state is equal to the current state and the stream has already emitted at least one state, the emission is skipped. This is intentional β€” it prevents redundant widget rebuilds and keeps your UI efficient.

The equality check uses Dart's == operator. For plain Dart classes, == compares object identity by default (memory address). Two separate instances of the same class with identical field values are not equal unless you override == and hashCode. The problem arrives the moment you introduce Equatable or override equality manually, because now two states with the same data are considered equal, and Bloc correctly skips them.

Here is the relevant behavior in pseudocode:

if (newState == currentState) {
  // Bloc skips the emit β€” no stream event, no rebuild
  return;
}
currentState = newState;
// stream event fires, BlocBuilder rebuilds

Understanding this single rule explains every scenario covered below.

Why Equatable Is Both the Solution and the Trap

Equatable is the standard way to simplify value equality in Dart. You extend Equatable, implement props, and two instances with the same props are treated as equal. That is exactly what you want for avoiding unnecessary rebuilds when state genuinely has not changed.

The trap is when you emit logically different state β€” maybe a re-triggered loading state, or a reset to the same default value β€” but the field values happen to be identical. Bloc sees equal states and skips. Your UI never knows anything happened.

// Example: Cubit resets to the same default
class SearchCubit extends Cubit<SearchState> {
  SearchCubit() : super(SearchState.initial());

  void reset() {
    emit(SearchState.initial()); // Silently skipped if already initial
  }
}

If the user taps "Clear" twice in a row, the second tap does nothing visible. The cubit ran, but the UI did not respond. That is the failure mode you need to fix.

Fix 1: Override == and hashCode Correctly

If you are not using Equatable, make sure your custom == and hashCode reflect your actual intent. A common mistake is a partial override that accidentally makes two meaningfully different states appear equal.

class ProductState {
  final List<Product> items;
  final bool isLoading;

  const ProductState({required this.items, required this.isLoading});

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is ProductState &&
          runtimeType == other.runtimeType &&
          isLoading == other.isLoading &&
          listEquals(items, other.items); // compare by content, not reference

  @override
  int get hashCode => Object.hash(isLoading, Object.hashAll(items));
}

Using listEquals from package:flutter/foundation.dart compares list contents rather than list identity. If you skip that and compare the list reference directly, replacing the list with a new instance containing the same items will look like a change when it is not β€” or the reverse: mutating the existing list in place will look like no change when it is.

Fix 2: Use copyWith to Force a New Instance

If your state does not use Equatable (identity comparison is the default), but you keep mutating the same object and re-emitting it, the state will always look equal to itself. The fix is a copyWith pattern that always produces a new object.

class FilterState {
  final String query;
  final SortOrder sort;

  const FilterState({required this.query, required this.sort});

  FilterState copyWith({String? query, SortOrder? sort}) => FilterState(
        query: query ?? this.query,
        sort: sort ?? this.sort,
      );
}

// In the Cubit:
void updateQuery(String q) {
  emit(state.copyWith(query: q)); // always a new instance
}

This works because without Equatable, the new instance has a different identity from the old one, so == returns false and Bloc emits. The moment you add Equatable, this stops working for cases where query and sort have not actually changed β€” which may be exactly what you want, or may be the bug you are fighting.

Fix 3: Add a Unique Timestamp or Sequence Field

Sometimes you genuinely need to re-emit the same logical state β€” for example, showing a snackbar in response to a repeated user action, or re-triggering an animation. The cleanest solution is to add a field that always differs between emissions.

class NotificationState extends Equatable {
  final String message;
  final int version; // increment on every emit

  const NotificationState({required this.message, required this.version});

  @override
  List<Object> get props => [message, version];
}

// In the Cubit:
void showMessage(String msg) {
  emit(NotificationState(
    message: msg,
    version: state.version + 1, // guaranteed to differ
  ));
}

A DateTime.now().microsecondsSinceEpoch timestamp works too, but an integer counter is cheaper and deterministic β€” easier to test. Keep the version field out of any field you use for business logic comparisons; it exists only to bust equality.

Fix 4: Use Equatable Props Intentionally

The props getter on Equatable lets you decide exactly which fields determine equality. You do not have to include every field. Fields excluded from props are ignored during comparison, so two states that differ only in those fields will be treated as equal β€” and two states that differ only in included fields will trigger an emit.

class CartState extends Equatable {
  final List<CartItem> items;
  final String? lastError;
  final DateTime lastUpdated; // excluded from equality on purpose

  const CartState({
    required this.items,
    this.lastError,
    required this.lastUpdated,
  });

  @override
  List<Object?> get props => [items, lastError];
  // lastUpdated is intentionally excluded
}

Conversely, if you want every emission of the same data to rebuild β€” include a version counter in props. This gives you precise control without removing Equatable entirely.

If you are dealing with broader state management patterns in Flutter, you may also run into similar issues with other libraries β€” the article on Flutter Riverpod provider not updating UI after state change covers the same class of problem from a different angle.

Fix 5: Switch to a Stream Wrapper for Granular Control

When your state model is complex and you need both efficient rebuilds for most fields and unconditional emission for specific events, separate the two concerns. Keep your main state in Bloc for the bulk of your UI, and expose a dedicated Stream for one-shot events like navigation, dialogs, and snackbars.

class CheckoutCubit extends Cubit<CheckoutState> {
  final _eventController = StreamController<CheckoutEvent>.broadcast();
  Stream<CheckoutEvent> get events => _eventController.stream;

  CheckoutCubit() : super(CheckoutState.initial());

  void placeOrder() async {
    // State update for loading indicator
    emit(state.copyWith(isLoading: true));
    final result = await _repository.submitOrder();
    emit(state.copyWith(isLoading: false));

    // One-shot event, bypasses Bloc equality check entirely
    _eventController.add(
      result.isSuccess
          ? CheckoutEvent.success(result.orderId)
          : CheckoutEvent.failure(result.error),
    );
  }

  @override
  Future<void> close() {
    _eventController.close();
    return super.close();
  }
}

In the widget, listen to cubit.events inside initState using a subscription, or use a StreamBuilder alongside your BlocBuilder. This pattern is sometimes called the "side-effect stream" approach and keeps your main state clean while giving you unconditional delivery for events that must always fire.

For related context on how Flutter handles lifecycle cleanup when state subscribers exist, see the guide on Flutter setState called after dispose β€” similar cleanup patterns apply to stream subscriptions opened in initState.

Common Pitfalls When Fixing This

A few mistakes come up repeatedly when developers address this issue:

  • Removing Equatable entirely to "fix" equality checks. This causes the opposite problem β€” your UI rebuilds on every emit even when nothing changed, which can tank performance in large widget trees.
  • Using mutable state objects. If you mutate an existing state object in place and re-emit it, Bloc may see two references to the same object and skip the emit, or worse, the comparison result is undefined. Always treat state as immutable.
  • Comparing lists by reference inside Equatable props. A List in props is compared with ==, which for lists compares identity, not contents. Use listEquals in a custom == override, or switch to a package like fast_immutable_collections that provides value-equal list types.
  • Forgetting that Bloc skips the first emission if it matches the initial state. If your initial state constructor and a later emit produce identical objects, the emit is silently dropped. Set a sensible initial state that differs from any emitted state, or use a sealed state hierarchy where the initial variant is structurally distinct.
  • Overusing version counters everywhere. If you add a version field to every state class, you effectively disable Bloc's deduplication entirely. Use this trick only for states where re-triggering is intentional.

You will encounter the same family of stale-state problems in other parts of Flutter development. If you have seen Flutter FutureBuilder rebuilding on every parent setState, the root cause there shares the same theme: the framework's rebuild decisions depend on how equality and identity are handled in your code.

If you are also managing routing in your app, be aware that navigation state managed by GoRouter can exhibit similar silent failures β€” the article on Flutter GoRouter redirects silently failing on deep links is worth a read if you are combining Bloc with GoRouter.

Wrapping Up

Flutter Bloc's equality check is a feature, not a bug β€” but it requires you to model state carefully. Here are the concrete steps to take right now:

  1. Reproduce the skip in isolation. Write a unit test that emits the suspect state twice and asserts the stream emits two events. If the test catches only one, you have confirmed the equality check is blocking you.
  2. Audit your props list. If you use Equatable, print your state's props before and after the problem emit. If they are identical, that is your culprit.
  3. Choose the right fix for your context. copyWith for most state updates; a version field for intentional re-emissions; a side-effect stream for one-shot events like snackbars and navigation.
  4. Keep state immutable. Dart has no built-in immutability enforcement, so enforce it through discipline β€” final fields, no setters, always produce new instances.
  5. Add a regression test. Once you fix the issue, lock in the behavior with a Cubit unit test using bloc_test's expect list. If someone changes the state model later, the test will catch the regression before it ships.

Frequently Asked Questions

Why does Flutter Bloc silently skip my emit when I call it twice with the same state?

Bloc compares the new state to the current state using the == operator before emitting. If they are equal, the emit is dropped to prevent redundant widget rebuilds. This is intentional behavior that you need to work around when re-emission is genuinely required.

How do I force Flutter Bloc to emit a state even if the value has not changed?

The most reliable approach is to add a version counter field to your state class and include it in Equatable props (or your custom == override), then increment it on every emit. This guarantees the new state differs from the old one, so Bloc emits unconditionally.

Does removing Equatable from my state class fix the duplicate state problem?

Yes, but it trades one problem for another. Without Equatable, Dart falls back to identity comparison, so every new state instance is considered different and always triggers a rebuild β€” even when nothing meaningful changed. That causes unnecessary widget rebuilds and can hurt performance.

What is the correct way to compare a List field inside a Flutter Bloc Equatable state?

Lists in Dart compare by reference by default, not by content. Inside a custom == override, use listEquals from package:flutter/foundation.dart to compare list contents. If you put a List directly in Equatable props, two different list instances with the same items will appear unequal, causing extra emits.

Can I use a side-effect stream alongside Flutter Bloc to handle one-shot UI events?

Yes, this is a well-established pattern. Add a broadcast StreamController to your Cubit, expose it as a stream, and push events like navigation triggers or snackbar messages to it directly. This bypasses Bloc's equality check entirely for those events while keeping your main state deduplication intact.

πŸ“€ Share this article

Sign in to save

Comments (0)

No comments yet. Be the first!

Leave a Comment

Sign in to comment with your profile.

πŸ“¬ Weekly Newsletter

Stay ahead of the curve

Get the best programming tutorials, data analytics tips, and tool reviews delivered to your inbox every week.

No spam. Unsubscribe anytime.