With package:bloc, when is a Cubit enough, and what would make you choose an event-driven Bloc instead?
answer
- both extend BlocBase
- methods versus event classes
- Change versus Transition
- EventTransformer per handler
- start with Cubit, scale up
basics
~20 sA 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 sBoth 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 linesimport '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
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.
Explain the two documented Bloc advantages, Transition-based traceability and per-handler EventTransformer, and why neither exists for a Cubit.
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.
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