With package:bloc, what is a Cubit, and how does it hold, change and publish its state?
answer
- extends Cubit<State>, not Bloc
- initial value passed to super
- emit is @protected
- state getter reads synchronously
- broadcast stream, no replay
basics
~20 sA 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 linesimport '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
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.
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.
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>.
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