skip to content

With package:bloc, when does a Cubit's emit silently drop a new state, and how does equatable change that decision?

level: middleimportance: should knowfreq 56%

answer

  1. uses the state's own ==
  2. equal to current: dropped
  3. first emit is exempt
  4. plain class: identity equality
  5. Equatable: runtimeType plus props

basics

~20 s

Cubit.emit drops a state that is == to the current one, unless nothing has been emitted yet. A plain class compares by identity, so every new instance gets through; extending Equatable makes value-equal states equal, so no-op emits are dropped.

solid answer

~40 s

In bloc 9, `emit` returns early when the new state `==` the current one and something has already been emitted, using the state type's own `operator ==`. For `int`, `String`, enums and records that is value equality. For a plain class it is identity: a fresh `WaterState(glasses: 3, dailyLimit: 8)` never equals the current instance, so every emit notifies listeners and rebuilds, even when nothing changed. Extending `Equatable` and listing every field in `props` makes `==` compare the runtime type and the props, so an emit that changes nothing is dropped and tests can compare expected instances. The first-emit exemption lets a Cubit announce a value equal to its initial state once. Leave Equatable out only when identical states back-to-back must notify twice.

code

dart · 16 lines
dart
import 'package:bloc/bloc.dart';

class PlainState {
  PlainState(this.glasses);
  final int glasses;
}

class PlainCubit extends Cubit<PlainState> {
  PlainCubit() : super(PlainState(0));

  // Always rebuilds: a new PlainState is never == the current one.
  void refresh() => emit(PlainState(state.glasses));

  // Never publishes after the first emit: the same instance is == itself.
  void touch() => emit(state);
}

go deeper

for a junior

Recall the rule: emit drops a state equal to the current one, and Equatable is how a class state gets value equality from its props.

for a middle

Explain the exact check, the first-emit exemption, what == means for primitives, records, plain classes and Equatable, and why every field must be in props.

for a senior

Show judgement on when deduplication hurts, such as a repeated identical error, and how you design states so real repeats still notify.

for a principal

Weigh a team-wide convention: value equality on every state for fewer rebuilds and simpler tests, against the few states that must repeat, and how reviews catch props omissions.

## The duplicate check inside emit In the **bloc** package (bloc 9.2), every `Cubit` inherits `emit` from `BlocBase`. After confirming the Cubit is not closed, `emit` runs one line that decides whether a state is published at all: ```dart if (state == _state && _emitted) return; ``` - `_state` is the **current** state; `_emitted` starts `false` and flips to `true` after the first successful emit. - When the check returns early, **nothing** happens: no `onChange`, no stream event, no rebuild in any widget listening through flutter_bloc. - The comparison is against the current state only, not history. Emitting A, then B, then A again publishes all three. - The **first emit is exempt** (since bloc 6.0), so a Cubit created with state 0 can still push 0 to listeners once. ## What == means for your state type `emit` does not inspect fields itself; it calls the state's `operator ==`. What that means depends on the type you chose: | State type | `==` compares | Emitting an unchanged value | |---|---|---| | `int`, `String`, `bool` | value | dropped | | enum value | the constant | dropped | | record such as `({int glasses, int limit})` | fields, structurally | dropped | | plain class, no override | identity | a new instance passes, the same instance is dropped | | `Equatable` subclass | runtime type and `props` | dropped | The plain-class row is where teams get surprised in both directions: a Cubit rebuilds on every emit even when nothing changed, and a Cubit that mutates and re-emits the same instance never rebuilds. ## Equatable in a Cubit state The **equatable** package overrides `==` and `hashCode` for you from a `props` list: ```dart import 'package:equatable/equatable.dart'; class WaterState extends Equatable { const WaterState({required this.glasses, required this.dailyLimit}); final int glasses; final int dailyLimit; WaterState copyWith({int? glasses, int? dailyLimit}) => WaterState( glasses: glasses ?? this.glasses, dailyLimit: dailyLimit ?? this.dailyLimit, ); @override List<Object?> get props => [glasses, dailyLimit]; } ``` With that state, a Cubit whose `addGlass()` clamps at the limit and emits `state.copyWith(glasses: state.glasses)` at 8 of 8 glasses produces an equal state, and `emit` drops it — no pointless rebuild. Things to know about Equatable's `==`: - `props` is typed `List<Object?>`, so nullable fields belong in it. - It also requires the same **runtime type**, so two different state classes with identical props are not equal. - `List`, `Set` and `Map` props are compared **element by element**, not by reference. - `Equatable` is annotated `@immutable`, so the analyzer warns about non-final fields in a subclass. - In equatable 3.0 (the pinned source) `EquatableMixin` was removed; `Equatable` is now an `abstract mixin class`, so a class that already extends something writes `with Equatable`. ## When deduplication works against you The bloc FAQ is explicit: do not use Equatable when you want the **same state back-to-back** to trigger twice. The classic case is an error: the user taps save, it fails, they tap again and it fails with the same message. With Equatable the second failure state equals the first, `emit` drops it, and a listener that shows a message never fires again. Options: 1. Emit a different state in between (for example a saving state), so the failure is no longer a repeat. 2. Add a field that changes each time, such as an attempt counter. 3. Leave that state class without value equality. ## A quick decision list 1. Primitive, enum or record state: value equality is already built in. 2. Class state: extend `Equatable` and put **every** distinguishing field in `props`. 3. Need repeated identical notifications: design the state so repeats differ, or skip value equality for it.

  • Does a Cubit's emit compare against every earlier state or only the current one?
    Only the current one: the check is `state == _state`. Emitting A, then B, then A again publishes all three; only an immediately repeated equal state is dropped. Suppressing a value shown several states ago is business logic that belongs in the Cubit's method, not in emit.
  • Why does equatable compare runtimeType as well as props?
    So that different state classes carrying the same fields are not equal. Two subclasses `WaterLoading` and `WaterIdle`, both with empty `props`, would otherwise compare equal, and emitting loading right after idle would be dropped. Equatable's `==` first requires `runtimeType == other.runtimeType`, then compares props.

saying these in an interview costs you the question

  • emit always notifies listeners, even when the state did not change
  • Without Equatable, a new instance with the same fields is dropped
  • Equatable makes states compare by reference instead of by value
  • Emitting a value equal to the initial state is always dropped
  • Equatable ignores runtimeType, so different subclasses with equal props are equal
  • emit drops any state that appeared anywhere in the Cubit's history