Flutter and Dart 3: Records, Patterns and Advanced Interview Questions

Master Dart 3 records, patterns, and sealed classes for Flutter development. Covers destructuring, exhaustive pattern matching, state modeling, and advanced interview questions with practical code examples.

Dart 3 records patterns and sealed classes for Flutter development

Dart 3 records, patterns, and sealed classes form the foundation of modern Flutter development in 2026. These three features, introduced in Dart 3.0 and refined through Dart 3.11, enable algebraic data types, exhaustive pattern matching, and compile-time safety that catches entire categories of bugs before runtime.

Key Takeaway

Records bundle multiple values without a class. Patterns destructure them in one line. Sealed classes guarantee exhaustive switch coverage at compile time. Together, they replace most boilerplate-heavy state modeling code.

Understanding Dart 3 Records: Lightweight Data Aggregation

Records solve a long-standing Dart problem: returning multiple values from a function without creating a dedicated class. A record is an anonymous, immutable, aggregate type.

user_repository.dartdart
// Returning multiple values with a positional record
(String, int) parseUserInput(String raw) {
  final parts = raw.split(':');
  return (parts[0].trim(), int.parse(parts[1]));
}

// Named fields improve readability for complex returns
({String name, String email, bool isVerified}) fetchUserProfile(String id) {
  // Simulated database lookup
  return (
    name: 'Alice Chen',
    email: 'alice@example.com',
    isVerified: true,
  );
}

Records are structurally typed: two records with the same field types and names are the same type. This eliminates the need for Tuple2, Pair, or similar utility classes that Flutter developers relied on before Dart 3.

records_equality.dartdart
// Structural equality — no need for custom == operator
void main() {
  final a = (1, 'hello');
  final b = (1, 'hello');
  print(a == b); // true — records compare by value

  // Named fields also support equality
  final profile1 = (name: 'Alice', role: 'admin');
  final profile2 = (name: 'Alice', role: 'admin');
  print(profile1 == profile2); // true
}

Records work best for function returns, local aggregation, and map keys. For domain models with methods and validation, classes remain the right choice.

Dart Pattern Matching: Destructuring and Control Flow

Pattern matching in Dart 3 goes beyond simple type checks. Patterns destructure values, bind variables, and participate in control flow — all in a single expression.

pattern_basics.dartdart
// Destructuring a record with pattern matching
void main() {
  final (name, email, isVerified) = fetchUserProfile('u-123');
  // name, email, isVerified are now local variables

  // List patterns with rest operator
  final scores = [98, 87, 92, 76, 84];
  final [first, second, ...remaining] = scores;
  print('Top two: $first, $second'); // 98, 87
  print('Others: $remaining');        // [92, 76, 84]
}

Switch expressions, combined with patterns, replace verbose if-else chains with concise, exhaustive matching:

pattern_switch.dartdart
// Switch expression with guard clauses
String classifyScore(int score) => switch (score) {
  >= 90 => 'Excellent',
  >= 80 => 'Good',
  >= 70 => 'Average',
  >= 60 => 'Below Average',
  _ => 'Needs Improvement',
};

// Object pattern matching with type checking
String describeValue(Object value) => switch (value) {
  int n when n < 0   => 'Negative integer: $n',
  int n              => 'Positive integer: $n',
  String s when s.isEmpty => 'Empty string',
  String s           => 'String of length ${s.length}',
  List l when l.isEmpty  => 'Empty list',
  List l             => 'List with ${l.length} elements',
  _                  => 'Unknown type',
};

Guard clauses (the when keyword) add conditional logic to patterns without nesting. This keeps switch expressions flat and readable.

Sealed Classes: Exhaustive State Modeling

Sealed classes restrict their subtype hierarchy to the same file, enabling the Dart compiler to verify that every subtype is handled in a switch. This is the critical piece for type-safe state management.

auth_state.dartdart
// Sealed class defining all possible authentication states
sealed class AuthState {}

class AuthInitial extends AuthState {}

class AuthLoading extends AuthState {}

class AuthAuthenticated extends AuthState {
  final String userId;
  final String displayName;
  AuthAuthenticated({required this.userId, required this.displayName});
}

class AuthError extends AuthState {
  final String message;
  final int? statusCode;
  AuthError({required this.message, this.statusCode});
}

The compiler enforces exhaustiveness — every subclass must be handled:

auth_widget.dartdart
// Exhaustive switch — compiler error if a case is missing
Widget buildAuthUI(AuthState state) => switch (state) {
  AuthInitial()       => const LoginPrompt(),
  AuthLoading()       => const CircularProgressIndicator(),
  AuthAuthenticated(
    displayName: final name
  )                   => Text('Welcome, $name'),
  AuthError(
    message: final msg,
    statusCode: final code,
  )                   => ErrorBanner(
    message: msg,
    code: code,
  ),
};

Adding a new AuthState subclass (e.g., AuthSessionExpired) immediately produces compile-time errors in every switch that handles AuthState. This eliminates the risk of forgotten state handling — a common source of bugs in Flutter apps using string-based or enum-based state.

Ready to ace your Flutter interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Combining Records, Patterns, and Sealed Classes in Flutter

The real power emerges when these three features work together. A practical pattern for Flutter API calls combines sealed classes for result types with records for structured data:

api_result.dartdart
// Generic sealed result type for API operations
sealed class ApiResult<T> {}

class ApiSuccess<T> extends ApiResult<T> {
  final T data;
  final ({int statusCode, Map<String, String> headers}) metadata;
  ApiSuccess(this.data, {required this.metadata});
}

class ApiFailure<T> extends ApiResult<T> {
  final String error;
  final int? statusCode;
  ApiFailure(this.error, {this.statusCode});
}
product_screen.dartdart
// Consuming the sealed result with pattern matching
Widget buildProductList(ApiResult<List<Product>> result) => switch (result) {
  ApiSuccess(
    data: final products,
    metadata: (statusCode: 200, headers: _),
  ) when products.isNotEmpty => ListView.builder(
    itemCount: products.length,
    itemBuilder: (_, i) => ProductCard(products[i]),
  ),
  ApiSuccess(data: final products) when products.isEmpty =>
    const EmptyState(message: 'No products found'),
  ApiFailure(statusCode: 401) =>
    const AuthExpiredBanner(),
  ApiFailure(error: final msg) =>
    ErrorDisplay(message: msg),
};

This pattern eliminates null checks, type casts, and runtime assertions. Every possible state is accounted for at compile time.

Advanced Interview Questions on Dart 3 Features

Technical interviews for Flutter positions in 2026 increasingly test understanding of Dart 3 language features. Below are questions that go beyond syntax and probe architectural thinking.

Q: How do records differ from classes for data modeling?

Records are structurally typed, immutable, and anonymous. Two records with identical field types are the same type. Classes are nominally typed — two classes with identical fields are different types. Records suit function returns, local aggregation, and map keys. Classes suit domain models requiring methods, validation, or identity.

Q: When would a sealed class hierarchy be preferred over an enum?

Enums carry no per-variant data. Sealed classes allow each subtype to hold different fields. For example, AuthAuthenticated carries userId while AuthError carries message and statusCode. Enums cannot model this. Sealed classes also support deep pattern matching on their fields, not just the variant name.

Q: What happens at compile time when a new subtype is added to a sealed class?

The Dart analyzer immediately flags every switch expression or statement handling that sealed type as non-exhaustive. The code does not compile until the new case is handled everywhere. This compile-time guarantee is the primary advantage over abstract classes, which provide no exhaustiveness checking.

Q: Explain the difference between _ in patterns versus _ as a wildcard variable (Dart 3.7+).

Before Dart 3.7, _ was a wildcard only inside patterns — var [_, _, third] = list worked because pattern _ never binds. Outside patterns, _ was a regular variable name. Dart 3.7 unified this: _ is now non-binding everywhere, including function parameters and local variables. Multiple _ declarations in the same scope no longer conflict.

Q: How does pattern matching improve null safety in practice?

Patterns enable concise null checking through if-case:

null_pattern.dartdart
// If-case for null-safe extraction
void processUser(Map<String, dynamic> json) {
  if (json case {'name': String name, 'age': int age}) {
    // name and age are non-nullable here
    print('$name is $age years old');
  } else {
    print('Invalid user data');
  }
}

The pattern simultaneously validates the map structure, checks types, and binds non-nullable variables. Without patterns, this requires nested null checks and manual type casts.

Migration Strategy: Adopting Dart 3 Features in Existing Flutter Apps

Migrating an existing Flutter codebase to Dart 3 patterns can be done incrementally. Start with the highest-value changes.

Step 1 — Replace Tuple/Pair types with records. Search for Tuple2, Pair, or custom two-field classes used only for bundling return values. Replace with records for immediate code reduction.

Step 2 — Convert if-else type checks to switch expressions. Any chain of if (value is TypeA) ... else if (value is TypeB) becomes a switch expression with pattern matching. This applies especially to widget building and state handling.

Step 3 — Seal state hierarchies. For BLoC states, Riverpod notifier states, or custom state classes, add the sealed modifier. The compiler then identifies every location where state handling is incomplete.

Step 4 — Adopt guard clauses in switches. Replace if checks inside case bodies with when guards for flatter, more readable control flow.

Each step is independently valuable and does not require modifying the entire codebase at once.

Dart 3.10 Dot Shorthands

Dart 3.10 (November 2025) introduced dot shorthands, allowing .value instead of EnumType.value when the type is known from context. This pairs well with pattern matching, reducing visual noise in switch expressions that handle enum-like types.

Records Are Not Classes

Records cannot have methods, custom constructors, or inheritance. Attempting to add behavior to a record type is a sign that a class or sealed class hierarchy is more appropriate. Use records strictly for data aggregation without behavior.

Conclusion

  • Records eliminate boilerplate for multi-value returns — use them instead of Tuple, Pair, or single-purpose classes
  • Pattern matching replaces if-else type check chains with exhaustive, compiler-verified switch expressions
  • Sealed classes guarantee that every state variant is handled, turning runtime bugs into compile-time errors
  • Guard clauses (when) keep switch expressions flat and eliminate nested conditionals inside case bodies
  • Dart 3.7 wildcard variables (_) unify the non-binding behavior of patterns with regular parameters and local variables
  • Migration can happen incrementally: start with records for function returns, then convert type checks to switches, then seal state hierarchies
  • These features are actively tested in Flutter technical interviews — understanding the "why" matters as much as the syntax

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Tags

#flutter
#dart
#pattern-matching
#sealed-classes
#records
#interview

Share

Related articles