DMarlon Studio

Lesson 11 · 11 of 20

Patterns and sealed classes · Dart

Model a screen's states with Dart 3 sealed classes and handle them with an exhaustive switch, object patterns, when guards and if-case.

Dart · Patterns and sealed classes in DMarlon Studio: the example's code and its output in the terminal

A `sealed` class can only have subtypes in its own file. It is abstract and, in exchange, the compiler knows the complete list of its subtypes: Loading, Loaded and Failed. It is the usual way to model a screen's states in Flutter: loading, showing data or showing an error.

render uses a `switch` expression with object patterns: Loaded(:final items) checks that the state is a Loaded and pulls its items field into a variable. Patterns can be more precise: Loaded(items: []) only matches an empty list, Failed(code: 401) only that exact code, and the guard when items.length == 1 adds a condition to the case.

Order matters: cases are tried from top to bottom and the first match wins, which is why the general Loaded(:final items) comes after the specific ones. And since the class is sealed, the switch is exhaustive: it needs no default, and if you delete the Loading() case or add a new state, the program will not compile until it is covered.

Patterns are not just for classes. The last line uses if (json case ...) with a map pattern and a list pattern: it checks that there is a user key holding a String and a roles list whose first element is text (... accepts the rest), and binds the values to name and role on the way. It is a safe way to read JSON without a chain of checks and casts.

Code · main.dart

// sealed: every subtype is declared in this file, so the compiler// knows the complete list and can check that a switch covers it.sealed class LoadState {}class Loading extends LoadState {}class Loaded extends LoadState {  final List<String> items;  Loaded(this.items);}class Failed extends LoadState {  final int code;  final String message;  Failed(this.code, this.message);}// Exhaustive switch: no default needed; forget a case and it won't compile.String render(LoadState state) => switch (state) {      Loading() => 'Showing spinner...',      Loaded(items: []) => 'Nothing here yet',      Loaded(:final items) when items.length == 1 => 'One item: ${items[0]}',      Loaded(:final items) => '${items.length} items: ${items.join(', ')}',      Failed(code: 401) => 'Session expired, please log in',      Failed(:final code, :final message) => 'Error $code: $message',    };void main() {  final states = [Loading(), Loaded([]), Loaded(['Milk']), Loaded(['Tea', 'Rice']),      Failed(401, 'Unauthorized'), Failed(503, 'Server busy')];  for (final state in states) print(render(state));  // Patterns also check and unpack a map, such as decoded JSON.  final Object json = {'user': 'carmen', 'roles': ['admin', 'editor']};  if (json case {'user': String name, 'roles': [String role, ...]}) print('$name is $role');}

Output · dart run main.dart

Showing spinner...
Nothing here yet
One item: Milk
2 items: Tea, Rice
Session expired, please log in
Error 503: Server busy
carmen is admin

Key points

  • sealed fixes the list of subtypes in the same file.
  • A switch over a sealed class is exhaustive: no default needed.
  • Class(:final field) checks the type and extracts the field.
  • when adds a condition to a case.
  • if (value case pattern) validates and destructures at once.

Exercise

Add an Offline state to the sealed class, look at the compiler error in render and add the case that prints No connection.