Sealed Classes for State
3 min read
State Management
| Without sealed states | With sealed states |
|---|---|
if (state is Loading) chains, easy to miss a case | switch over all cases, compiler-enforced |
default: branches hide forgotten states | No default needed — compiler checks |
| Adding a state silently breaks UIs | Adding a state breaks the build |
State has booleans like isLoading, isError | Each state is its own type with the right fields |
A sealed state class is the right tool for "this thing is in exactly one of N modes, each carrying different data."
Code in action
sealed class NetworkState<T> {}
class Initial<T> extends NetworkState<T> {}
class Loading<T> extends NetworkState<T> {}
class Success<T> extends NetworkState<T> { final T data; Success(this.data); }
class Failure<T> extends NetworkState<T> { final String error; Failure(this.error); }
// Exhaustive — no `default`, compiler proves all cases handled
Widget render(NetworkState<User> s) => switch (s) {
Initial() => const Text('Press load'),
Loading() => const CircularProgressIndicator(),
Success(:final data) => UserCard(data),
Failure(:final error) => ErrorBanner(error),
};
// Add a new state…
class Refreshing<T> extends NetworkState<T> { final T staleData; Refreshing(this.staleData); }
// Every `switch` over NetworkState fails to compile until you handle Refreshing.
// Refactoring becomes safe.
When sealed states pay off most
| Situation | Why |
|---|---|
| Async data with init / loading / success / error | Classic ADT — sealed is perfect |
| Auth flow (out / loading / in / failed) | Different payloads per state |
| Pagination (initial / loading next / has more / done) | Easy to forget edge states without exhaustiveness |
| Multi-step forms / wizards | One sealed step-state per phase |
| State machines in general | Every transition is a new state subtype |
Keep enums for labels without payloads (e.g. enum SortOrder { asc, desc }). Reach for sealed when each variant carries different data.
Common mistakes to avoid
// ❌ Adding `default:` and undoing exhaustiveness
switch (state) {
case Loading(): return ...;
default: throw 'unreachable';
}
// ✅ Drop the default — the compiler now tells you when you've missed a case
// ❌ Booleans masquerading as states
class UiState { bool isLoading; bool isError; User? data; String? error; }
// Mutually exclusive states modelled as parallel booleans → invalid combinations possible
// ✅ Use sealed states, one per mode
// ❌ Sealed without exhaustive switch — defeats the purpose
if (state is Success) ... else if (state is Failure) ... else ...
// ✅ switch (state) { Success() => ..., Failure() => ..., ... }
// ❌ Subclasses in different libraries
// lib/auth/state.dart: sealed class AuthState {}
// lib/auth/extra.dart: class ExtraState extends AuthState {} ❌
// Sealed subtypes must live in the same library
// ❌ Sealing classes that don't need exhaustive handling
// → adds friction with no benefit. Seal when you actually switch on it.
Interview follow-ups
-
Why does Dart require sealed subtypes to live in the same library? So the compiler can prove the set of subtypes is closed. If external packages could extend a sealed class, the compiler couldn't enumerate cases — the exhaustiveness guarantee would silently break downstream.
-
Sealed class vs enhanced enum — when to use which? Enums for finite labels with no payload (
Status.idle). Sealed classes when each case carries its own data (Loading()vsSuccess(user)vsFailure(error)). Dart 3 enhanced enums can hold fields, blurring the line, but the rule of thumb: payload variability → sealed. -
What changes about your
BlocBuilder/ConsumerWidgetcode when you sealed your state? You replaceif (state is X)chains withswitch (state), drop thedefaultbranch, and rely on the compiler to flag missed cases. Pattern matching with:final datadestructures fields cleanly inline. -
How do sealed states interact with
==andhashCode? Same as any class — by default they use identity. For state classes you usually want value equality soBlocBuilderand Provider'sselectcan de-dupe correctly. Pair sealed states withEquatableorfreezedfor automatic value equality.
How helpful was this content?
Please sign in to rate this article.