skip to content

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

level: middleimportance: must knowfreq 68%

answer

  1. both extend BlocBase
  2. methods versus event classes
  3. Change versus Transition
  4. EventTransformer per handler
  5. start with Cubit, scale up

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.

solid answer

~40 s

Both extend `BlocBase`, so they share `state`, `stream`, emit's duplicate rule, `close()` and every flutter_bloc widget. A Cubit is less code: a state type plus methods, and the methods act as the events. A Bloc makes each input an event object handled by `on<E>`, which buys two things. **Traceability**: `onTransition` receives a `Transition` carrying `currentState`, `event` and `nextState`, so logs say *why* the state changed, while a Cubit's `Change` says only what changed. **Event transformation**: each handler can take an `EventTransformer`, such as a debounce or bloc_concurrency's `droppable` or `restartable`. The library's own advice is to start with a Cubit and refactor to a Bloc when you need either.

code

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

// Cubit: the method is the input.
class WaterIntakeCubit extends Cubit<int> {
  WaterIntakeCubit() : super(0);

  void addGlass() {
    if (state < 8) emit(state + 1);
  }
}

// Bloc: the event is the input.
sealed class WaterEvent {}

final class GlassAdded extends WaterEvent {}

class WaterIntakeBloc extends Bloc<WaterEvent, int> {
  WaterIntakeBloc() : super(0) {
    on<GlassAdded>((event, emit) {
      if (state < 8) emit(state + 1);
    });
  }
}

go deeper

for a junior

Recall the one-line difference: a Cubit changes state through methods, a Bloc through events handled by on<E>, and both work with the same widgets.

for a middle

Explain the two documented Bloc advantages, Transition-based traceability and per-handler EventTransformer, and why neither exists for a Cubit.

for a senior

Show when the extra ceremony pays off in production: auditing critical state changes, taming rapid inputs, and what a Cubit-to-Bloc refactor actually touches.

for a principal

Set a team default and the trigger for deviating from it, and decide whether a lint rule should enforce the choice across features.

## What the two share In the **bloc** package (bloc 9.2), `Cubit<State>` and `Bloc<Event, State>` both extend `BlocBase<State>`. That base gives them the same outward surface: - a synchronous `state` getter and a broadcast `stream`; - the same duplicate rule inside `emit` (a state `==` to the current one is dropped after the first emit); - `close()`, `isClosed`, `onChange` and `onError`; - compatibility with every flutter_bloc widget — `BlocBuilder`, `BlocListener` and friends accept either. So the choice is about **how input arrives**, not about what the UI can do with the result. ## What a Cubit gives you - **Less code**: define the state and the methods that change it. No event classes, no handler registration. - **Methods act as events**: `cubit.addGlass()` is the input; the method body reads `state` and calls `emit`. - **Awaitable calls**: a `Future<void>` method lets the caller await completion, which is convenient for a pull-to-refresh or a test. - **A good fit** for counters, toggles, form fields, a single fetch-and-show screen — anything where the cause of a change is obvious from the method name. ## What a Bloc adds 1. **Traceability.** Every input is an event object passed to `add`. Before each state change a Bloc calls `onTransition` with a `Transition` — which extends `Change` and adds the `event`. For critical state, such as whether a user is authenticated, logs then show whether the session ended because of a logout tap or a revoked token. A Cubit's `Change` records only `currentState` and `nextState`. 2. **Event transformation.** Events flow through a stream, and each `on<E>` registration can take a `transformer`. That is how a live search debounces keystrokes, or how a refresh drops taps while one is in flight. Bloc processes events **concurrently by default**; bloc_concurrency provides `sequential`, `droppable` and `restartable` transformers. 3. **Events as data.** Event classes can be logged, compared and replayed, which helps when reproducing a bug report. ## Side by side | Concern | Cubit | Bloc | |---|---|---| | Input | method call | event object via `add` | | Change record | `Change` (current, next) | `Transition` (current, event, next) | | Timing control | hand-written in methods | `EventTransformer` per handler | | Boilerplate | state + methods | state + events + handlers | | Widgets | all flutter_bloc widgets | all flutter_bloc widgets | ## Choosing 1. Default to a **Cubit** — the bloc documentation says to start with one when unsure and refactor or scale up to a Bloc as needed. 2. Move to a **Bloc** when you need an audit trail of *causes*, not just outcomes. 3. Move to a **Bloc** when you need to shape the timing of inputs: debounce, throttle, drop or restart. 4. Otherwise stay with the Cubit; extra ceremony without either need is cost with no return. ## What refactoring costs Moving a `WaterIntakeCubit` to a `WaterIntakeBloc` keeps the state classes unchanged. Each public method becomes an event class plus an `on<E>` handler, and call sites change from `addGlass()` to `add(GlassAdded())`. Tests change from calling methods to adding events. Because the widgets consume `BlocBase` either way, builders and listeners keep working. Teams that want one style everywhere can enable bloc_lint's `prefer_cubit` or `prefer_bloc` rule; neither is in the recommended set, and both report at info severity.

  • Can a Cubit debounce rapid calls the way a Bloc handler can?
    Not through bloc's API: an `EventTransformer` shapes a Bloc's event stream, and a Cubit has no event stream. You would hand-roll a `Timer` inside the Cubit and cancel it in `close()`. Needing that kind of timing control is one of the documented signals to move to a Bloc.
  • Does a Cubit serialise overlapping calls to the same async method?
    No. Two calls to `syncToday()` both run, interleaving at each `await`, and whichever finishes last emits last — possibly the stale result. Guard with an in-flight flag or a request token, or move to a Bloc and give that handler a `restartable` or `droppable` transformer from bloc_concurrency.

saying these in an interview costs you the question

  • BlocBuilder only works with a Bloc, not with a Cubit
  • Cubit is deprecated; the library now recommends Bloc everywhere
  • A Bloc processes events one at a time by default
  • A Cubit's logs show what triggered each change, just like a Bloc's
  • Switching from Cubit to Bloc means rewriting every state class