skip to content

Cubit Classes

A Cubit exposes methods that call emit with a new immutable state, so each UI change is one value on a stream. Interviewers ask when a Cubit is enough and when event-driven Bloc is needed.

on this pageshow

explore

questions

6

With package:bloc, what is a Cubit, and how does it hold, change and publish its state?

level: juniorimportance: must knowfreq 74%

answer

  1. extends Cubit<State>, not Bloc
  2. initial value passed to super
  3. emit is @protected
  4. state getter reads synchronously
  5. broadcast stream, no replay

basics

~20 s

A Cubit is a bloc-package state holder: it starts from the value passed to super, changes only when its own methods call emit, exposes the current value through state and publishes each later change on a broadcast stream.

solid answer

~40 s

`Cubit<State>` extends `BlocBase<State>`, the same base `Bloc` uses. Its only constructor takes the initial state, so a subclass writes `: super(0)` and `state` is never unset. Public methods such as `addGlass()` compute the next value and call `emit(next)`; `emit` is annotated `@protected`, so only the Cubit itself should call it. Consumers read the current value synchronously through `state` and subscribe to `stream`, a broadcast stream that delivers only states emitted after `listen` — it never replays the current one. `emit` drops a state equal to the current one (after the first emit), and after `close()` it throws a `StateError`. In Flutter, widgets usually consume a Cubit through flutter_bloc rather than raw `stream.listen`.

code

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

class WaterIntakeCubit extends Cubit<int> {
  WaterIntakeCubit({required int dailyLimit})
      : _dailyLimit = dailyLimit,
        super(0);

  final int _dailyLimit;

  void addGlass() {
    if (state >= _dailyLimit) return;
    emit(state + 1);
  }
}

Future<void> main() async {
  final cubit = WaterIntakeCubit(dailyLimit: 8);
  print(cubit.state); // 0
  final sub = cubit.stream.listen((glasses) => print('stream: $glasses'));
  cubit.addGlass();
  print(cubit.state); // 1, already updated
  await Future<void>.delayed(Duration.zero); // prints stream: 1
  await sub.cancel();
  await cubit.close();
}

go deeper

for a junior

Recall the four pieces: Cubit<State> with the initial value passed to super, methods that call emit, the state getter and the stream getter. Be able to write a small counter Cubit from memory.

for a middle

Explain emit's sequence: closed check, duplicate check, onChange, synchronous assignment, then stream delivery, and why a listener that subscribes late must read state first.

for a senior

Show you manage the lifecycle and API surface: who closes a hand-built Cubit, why emit stays inside the class, and why public methods return void or Future<void>.

for a principal

Frame the Cubit as a team contract: one owner per piece of state, intent-named methods as the only write path, and lint rules that keep the contract enforced across features.

## What a Cubit is In the **bloc** package (bloc 9.2), `Cubit<State>` is the simpler of the two state holders. It is an abstract class that extends `BlocBase<State>` — the same base class `Bloc` extends — and adds nothing but a constructor that takes the **initial state**. Everything else is inherited from `BlocBase`: holding the current value, `emit`, the state stream, error reporting and `close()`. A Cubit has **no events**. Callers invoke ordinary methods on it, such as `addGlass()` or `reset()`, and those methods decide the next state and hand it to `emit`. The type parameter can be anything: an `int` for a counter, an enum, a record, or an immutable class holding several fields. ## Anatomy of a Cubit A counter-and-limit Cubit for a water-intake tracker shows every moving part: ```dart import 'package:bloc/bloc.dart'; class WaterIntakeCubit extends Cubit<int> { WaterIntakeCubit({required int dailyLimit}) : _dailyLimit = dailyLimit, super(0); final int _dailyLimit; void addGlass() { if (state >= _dailyLimit) return; emit(state + 1); } void reset() => emit(0); } ``` - **Initial state** — `super(0)` is mandatory, because `Cubit`'s only constructor requires it. Very old bloc releases let you override an `initialState` getter; bloc 5.0 removed that in favour of passing the value to `super`. - **Methods as the input** — `addGlass()` reads `state`, applies the business rule (never exceed the limit) and emits. - **`emit` as the only write path** — it is annotated `@protected` from `package:meta`, so the analyzer reports `invalid_use_of_protected_member` when code outside the Cubit calls it. It is an annotation, not a language keyword: a lint-level guard rather than a compile error. ## What consumers use | Member | Kind | What it gives you | |---|---|---| | `state` | getter | The current value, read synchronously at any time | | `stream` | getter | A broadcast `Stream<State>` of states emitted after you subscribe | | `isClosed` | getter | Whether `close()` has been called | | `close()` | method | Returns a `Future<void>`; closes the state stream for good | The `stream` is backed by a broadcast `StreamController`, so any number of listeners can subscribe, but **it does not replay the current state**. The bloc documentation says it plainly: only subsequent state changes are received when calling `listen`. Code that needs the current value reads `state` first — which is what flutter_bloc's builder widgets do for their first build before following the stream. ## What happens inside emit `BlocBase.emit` runs a short, fixed sequence: 1. If the internal stream controller is closed, it throws `StateError('Cannot emit new states after calling close')`. 2. If the new state `==` the current state **and** something has been emitted before, it returns without doing anything. 3. It calls `onChange` with a `Change` holding `currentState` and `nextState`. 4. It assigns the new value, so `state` returns it **synchronously**. 5. It adds the value to the stream; listeners receive it asynchronously. Because step 4 precedes step 5, `cubit.state` already holds the new count the moment `addGlass()` returns, even though a `stream.listen` callback has not run yet. ## Lifecycle A Cubit owns a stream controller, so it must be closed when its feature goes away. A Cubit created by flutter_bloc's `BlocProvider(create: ...)` is closed by that provider; one you construct by hand — in a test, a service or `main` — needs an explicit `await cubit.close()`. ## Beginner mistakes to avoid - Mutating a field of the current state and calling `emit(state)`: the same instance is `==` to itself, so the emit is dropped. - Expecting `stream.listen` to deliver the current value. - Calling `emit` from a widget instead of exposing an intent-named method. - Returning results from public Cubit methods: bloc_lint's recommended rule `prefer_void_public_cubit_methods` asks for `void`, `Future<void>` or `FutureOr<void>`, because results should flow out as states.

  • What happens if a Flutter widget calls cubit.emit directly?
    It compiles: `@protected` from `package:meta` is an analyzer annotation, so the analyzer reports `invalid_use_of_protected_member` rather than the compiler refusing. The real damage is bypassing the Cubit's rules — a widget could push 12 glasses past an 8-glass limit. Expose intent-named methods such as `addGlass()` and keep `emit` inside the class.
  • Should a public Cubit method return the new state or a result value?
    The bloc_lint rule `prefer_void_public_cubit_methods`, part of its recommended set, warns on public Cubit methods whose return type is not `void`, `Future<void>` or `FutureOr<void>`. Results belong in emitted states so every consumer sees them; returning `Future<void>` still lets a caller await completion, for example in a pull-to-refresh.

A Cubit is a scoreboard with one operator. Only the operator (the Cubit's methods) changes the score; anyone can glance at the board for the current number (state); fans who start following the updates (stream) hear only the changes made after they tuned in, so to know the score right now they look at the board.

saying these in an interview costs you the question

  • A Cubit's state is null until the first emit
  • Listening to cubit.stream delivers the current state first
  • Cubits take input through add(event), exactly like Bloc
  • emit is private, so a widget calling it cannot compile
  • The state getter lags until stream listeners have run
open as a page

With package:bloc, when is a Cubit enough, and what would make you choose an event-driven Bloc instead?

level: middleimportance: must knowfreq 68%

basics

~20 s

A Cubit is enough when state changes are direct method calls. Choose a Bloc when you need each change traced to the event that caused it, or need to transform incoming events, such as debouncing a search or dropping overlapping requests.

open as a page

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%

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.

open as a page

With package:bloc, should a Cubit's state be one class with a status enum or a sealed class hierarchy, and why?

level: middleimportance: should knowfreq 44%

basics

~20 s

Use a sealed hierarchy when states are mutually exclusive and carry different data, for type safety and exhaustive switches; use one class with a status enum when states share data, such as keeping loaded values visible during an error.

open as a page

With package:bloc, why does an async Cubit method throw a StateError about emitting after close, and how do you prevent it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

The Cubit was closed, usually when its screen was disposed, while the method sat at an await; the next emit throws StateError('Cannot emit new states after calling close'). Check isClosed after each await and cancel owned subscriptions in close().

open as a page

A flutter_bloc water-intake Cubit appends a drink to a List inside its Equatable state and emits, but BlocBuilder does not rebuild. What is wrong?

level: seniorimportance: should knowfreq 46%

basics

~20 s

The Cubit mutated the list the current state already holds, so after any earlier emit the new state's props equal the current ones and emit drops it. Emit a state built from a new list, such as [...state.drinks, drink].

open as a page