Lesson 7 of 77 intermediate

Records, Patterns, Sealed Classes & Class Modifiers

Modern Dart 3.x — The Features Interviewers Love to Ask

Open interactive version (quiz + challenge)

Real-world analogy

Records are like labeled envelopes — (String name, int age) is an envelope with two labeled slots. Patterns are like a mail sorter that opens envelopes and routes them automatically. Sealed classes are like an enum that can carry different data — a traffic light that's Red, Yellow, or Green, each with its own brightness level.

What is it?

Dart 3 introduced records (lightweight multi-value returns), patterns (destructuring and matching), sealed classes (closed type hierarchies with exhaustive checking), and class modifiers (base, interface, final, mixin). These features modernize Dart significantly and are increasingly tested in senior interviews.

Real-world relevance

In a fintech app handling refund claims, sealed classes model the claim lifecycle: sealed class ClaimState {}. class Draft extends ClaimState {}, class Submitted extends ClaimState { final DateTime at; }, class Approved extends ClaimState { final double amount; }, class Rejected extends ClaimState { final String reason; }. Exhaustive switch ensures every state renders correctly in the UI with no missing cases.

Key points

Code example

// Records, Patterns, Sealed Classes — Dart 3.x

// --- RECORDS ---

// Positional record
(String, int) getUser() => ('Alice', 25);

// Named record fields
({String name, int age, String email}) getUserInfo() =>
    (name: 'Alice', age: 25, email: 'alice@mail.com');

// Destructuring records
void example() {
  final (name, age) = getUser();
  print('$name is $age'); // Alice is 25

  final (:name, :age, :email) = getUserInfo(); // Named destructuring
  print('$name ($email)');
}

// --- PATTERN MATCHING ---

// Switch expression (Dart 3)
String describe(Object value) => switch (value) {
  int x when x > 0 => 'Positive int: $x',
  int x when x < 0 => 'Negative int: $x',
  0               => 'Zero',
  String s        => 'String of length ${s.length}',
  (int a, int b)  => 'Record: ($a, $b)',
  _               => 'Unknown type',
};

// Object pattern for JSON parsing
void parseJson(Map<String, dynamic> json) {
  if (json case {'name': String name, 'age': int age, 'scores': List scores}) {
    print('Name: $name, Age: $age, Scores: $scores');
  } else {
    print('Invalid JSON structure');
  }
}

// --- SEALED CLASSES ---

sealed class NetworkResult<T> {
  const NetworkResult();
}

class Success<T> extends NetworkResult<T> {
  final T data;
  const Success(this.data);
}

class Failure<T> extends NetworkResult<T> {
  final String message;
  final int? statusCode;
  const Failure(this.message, {this.statusCode});
}

class Loading<T> extends NetworkResult<T> {
  const Loading();
}

// Exhaustive switch — compiler warns if you miss a case!
Widget buildUI(NetworkResult<User> result) => switch (result) {
  Success(:final data) => UserCard(user: data),
  Failure(:final message) => ErrorWidget(message: message),
  Loading()              => const CircularProgressIndicator(),
  // No default needed! Compiler knows all cases.
};

// --- SEALED FOR BLOC STATES ---

sealed class AuthState {
  const AuthState();
}

class AuthInitial extends AuthState {
  const AuthInitial();
}

class AuthLoading extends AuthState {
  const AuthLoading();
}

class Authenticated extends AuthState {
  final User user;
  final String token;
  const Authenticated({required this.user, required this.token});
}

class Unauthenticated extends AuthState {
  final String? reason;
  const Unauthenticated({this.reason});
}

// --- CLASS MODIFIERS ---

base class Animal {}          // Can extend, can't implement
interface class Printable {}  // Can implement, can't extend
final class Config {}         // Can't extend or implement outside file
mixin class Validator {}      // Can be used as class or mixin

Line-by-line walkthrough

  1. 1. Positional record — returns two values without creating a class
  2. 2. Named record fields — more readable, accessed by name
  3. 3. Destructuring a positional record into separate variables
  4. 4. Named destructuring with :name syntax — extracts matching fields
  5. 5. Switch expression — pattern matches on type and value
  6. 6. Guard clause with 'when' — matches only when condition is true
  7. 7. Object pattern — checks JSON structure AND extracts values in one step
  8. 8. Sealed class definition — only subtypes in this file are allowed
  9. 9. Success subtype carries generic data
  10. 10. Failure subtype carries message and optional status code
  11. 11. Exhaustive switch — compiler ensures all subtypes are handled
  12. 12. Destructuring in switch case — ':final data' extracts the data field
  13. 13. Sealed class for BLoC state — each state carries different data
  14. 14. Class modifiers control how types can be used externally

Spot the bug

sealed class AppState {}
class Loading extends AppState {}
class Loaded extends AppState { final List<String> items; Loaded(this.items); }
class Error extends AppState { final String message; Error(this.message); }

String render(AppState state) => switch (state) {
  Loading() => 'Loading...',
  Loaded(:final items) => 'Got ${items.length} items',
};
Need a hint?
Is this switch exhaustive? Are all sealed subtypes handled?
Show answer
The switch is not exhaustive — the Error case is missing. The compiler will flag this: 'The type AppState is not exhaustively matched by the switch cases since it doesn't match Error.' Fix: add 'Error(:final message) => "Error: $message"' to the switch.

Explain like I'm 5

Records are like a lunchbox with labeled compartments — one for your sandwich (name) and one for your juice (age). Patterns are like a sorting hat that looks inside your lunchbox and says 'Ah, you have a sandwich AND juice, so you go to Table 3!' Sealed classes are like a box of crayons where the factory decides exactly which colors are inside — Red, Blue, Green, and NO other color can sneak in.

Fun fact

Dart's sealed classes and pattern matching were heavily inspired by Kotlin's sealed classes and Swift's enums with associated values. The Dart team spent over 2 years designing these features, and they landed in Dart 3.0 (May 2023) as the biggest language update in Dart's history.

Hands-on challenge

Model a payment system using sealed classes: sealed class PaymentState with subtypes Idle, Processing(amount), Completed(transactionId, amount), Failed(error, canRetry). Write a function that takes PaymentState and returns appropriate UI description strings using an exhaustive switch expression.

More resources

Open interactive version (quiz + challenge) ← Back to course: Flutter Interview Mastery