With package:bloc, should a Cubit's state be one class with a status enum or a sealed class hierarchy, and why?
answer
- exclusive states or shared data
- status enum keeps old data
- sealed gives type safety
- exhaustive switch in widgets
- copyWith on the single class
basics
~20 sUse 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.
solid answer
~40 sThe bloc docs describe both as optional recommendations. **One class plus a status enum**: an immutable class with a `status` field (`initial`, `loading`, `success`, `failure`), the shared fields and `copyWith`. It is concise and keeps old data while loading or after an error, but it is not type safe — nothing stops you emitting `success` with its data still null. **A sealed class plus subclasses**: `sealed class WaterState` with `final class` subclasses such as `WaterLoading`, `WaterLoaded` and `WaterFailure`. Each state carries only its own fields, and a `switch` over the sealed type is checked for exhaustiveness. The cost is verbosity, and data does not survive a transition unless you copy it into the next state. Choose by whether the states are genuinely exclusive.
code
dart · 11 linesimport 'package:flutter/material.dart';
// WaterState, WaterLoading, WaterLoaded and WaterFailure as declared above.
Widget buildIntake(WaterState state) {
return switch (state) {
WaterLoading() => const CircularProgressIndicator(),
WaterLoaded(:final glasses, :final dailyLimit) =>
Text('$glasses of $dailyLimit glasses'),
WaterFailure(:final message) => Text(message),
};
}go deeper
Recall the two shapes: one class with a status enum and copyWith, or a sealed base with one subclass per state.
Explain the trade-off: shared data and brevity against type safety and exhaustive switches, and how each affects the Cubit's methods.
Show you pick per feature, e.g. a status enum where an error must sit over stale data and sealed states where phases are exclusive, and how you avoid nullable-field bugs.
Decide a codebase-wide default and the documented exceptions, weighing compile-time safety against boilerplate across dozens of features.
## The question behind the choice A Cubit in the **bloc** package (bloc 9.2) can hold any type as its state. Once a screen has loading, success and failure phases, you must decide how to represent them. The bloc documentation's modelling page presents two approaches — explicitly as recommendations, not rules — and the deciding question is: **are the states mutually exclusive, or do they share data?** ## Approach 1 — one class with a status enum ```dart enum WaterStatus { initial, loading, success, failure } class WaterState extends Equatable { const WaterState({ this.status = WaterStatus.initial, this.glasses = 0, this.dailyLimit = 8, this.errorMessage, }); final WaterStatus status; final int glasses; final int dailyLimit; final String? errorMessage; // copyWith omitted for brevity @override List<Object?> get props => [status, glasses, dailyLimit, errorMessage]; } ``` - **Pros**: simple and concise; every property is always reachable; `copyWith(status: WaterStatus.failure)` keeps `glasses` visible under an error banner. - **Cons**: **not type safe** — you can emit a malformed state such as `success` with missing data; state-specific properties end up nullable and need checks; the class tends to bloat as features grow. ## Approach 2 — a sealed class and subclasses ```dart sealed class WaterState extends Equatable { const WaterState(); @override List<Object?> get props => []; } final class WaterLoading extends WaterState { const WaterLoading(); } final class WaterLoaded extends WaterState { const WaterLoaded({required this.glasses, required this.dailyLimit}); final int glasses; final int dailyLimit; @override List<Object?> get props => [glasses, dailyLimit]; } final class WaterFailure extends WaterState { const WaterFailure(this.message); final String message; @override List<Object?> get props => [message]; } ``` - **Pros**: **type safe** — `glasses` exists only on `WaterLoaded`, so it cannot be read in the wrong phase; explicit about which data belongs to which state; a `switch` over `WaterState` in a widget is **exhaustive**, so adding a subclass makes the compiler point at every place that must handle it. - **Cons**: verbose (a base plus one class per state); shared properties may be duplicated; data does not carry over between states unless you copy it. ## Side by side | Concern | Status enum | Sealed hierarchy | |---|---|---| | Type safety | weak — nullable fields | strong — fields per state | | Keeping old data on error | free via `copyWith` | must be copied explicitly | | Exhaustive handling | switch on the enum | switch on the type | | Code volume | low | higher | | Best for | overlapping states | exclusive states | ## How the Cubit's methods change With a status enum, methods mostly call `emit(state.copyWith(...))`. With a sealed hierarchy, a method must first check which state it is in, typically with a Dart 3 pattern: ```dart void addGlass() { if (state case WaterLoaded(:final glasses, :final dailyLimit) when glasses < dailyLimit) { emit(WaterLoaded(glasses: glasses + 1, dailyLimit: dailyLimit)); } } ``` That check is a feature: the method cannot add a glass while the screen is still loading. ## Mixing and variations 1. A sealed base can carry shared fields, so `WaterFailure` can hold the last `glasses` if the UI must keep showing them. 2. The bloc docs point to the `final` modifier for teams that do not want exhaustive switching, or want to add subtypes later without breaking callers; a switch over a non-sealed type needs a wildcard case, so a new subtype breaks nothing. 3. Either approach pairs with Equatable so duplicate emits are dropped; with sealed classes, Equatable's runtime-type check keeps different subclasses unequal.
- How does each approach keep the last loaded count visible when a refresh fails?With a status enum, emit `state.copyWith(status: WaterStatus.failure, errorMessage: ...)` and `glasses` stays in the state untouched. With a sealed hierarchy, `WaterFailure` must carry the data itself, for example a `lastGlasses` field copied from the previous `WaterLoaded`; otherwise the failure state has nothing to show beneath the error.
- What happens to widgets when a new subclass is added to a sealed Cubit state?Every `switch` over the sealed type without a wildcard case stops compiling because it is no longer exhaustive. That is the point: the compiler lists every screen that must decide how to render the new state, instead of a runtime fallthrough discovered by a user.
saying these in an interview costs you the question
- A status enum makes it impossible to emit a malformed state
- Sealed subclasses automatically keep the previous state's data
- The bloc library mandates sealed classes for every Cubit state
- Sealed state classes cannot extend Equatable
- An exhaustive switch over sealed states still needs a default branch