Claude Cursor Skill

dart-3-updates

Use when writing switch statements, refactoring if-else chains, creating data classes, choosing records vs classes, destructuring values, or modernizing pre-Dart-3 code.

LLM Mart · 0 points · 16 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download evanca-flutter-ai-rules-skills_dart-3-updates-7d226d8.zip · 3 KB
Part of evanca/flutter-ai-rules — 37 skills

Install

skills CLI npx skills add https://github.com/evanca/flutter-ai-rules/tree/main/skills/dart-3-updates
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install evanca-flutter-ai-rules@llmmart
Git git clone https://github.com/evanca/flutter-ai-rules.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole evanca/flutter-ai-rules collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Dart 3 Updates Skill

Apply Dart 3 language features — branches, patterns, pattern types, and records — correctly and idiomatically.

When to Use

Use this skill when:

  • Writing or refactoring switch statements or if-else chains.
  • Creating new data-holding classes and deciding between sealed classes, records, or plain classes.
  • Destructuring values from maps, lists, records, or objects.
  • Modernizing pre-Dart-3 code to use patterns, exhaustiveness checks, or switch expressions.

1. Branches

if / if-case

// Standard if
if (score >= 90) {
  grade = 'A';
} else if (score >= 80) {
  grade = 'B';
} else {
  grade = 'C';
}

// if-case: match and destructure against a single pattern
if (pair case [int x, int y]) {
  print('$x, $y');
}
  • if conditions must evaluate to a bool.
  • In if-case, variables declared in the pattern are scoped to the matching branch.
  • If the pattern does not match, control flows to the else branch (if present).

switch statements

switch (command) {
  case 'quit':
    quit();
  case 'start' || 'begin': // logical-or pattern
    startGame();
  default:
    print('Unknown command');
}
  • Each matched case body executes and jumps to the end — break is not required.
  • Non-empty cases can end with continue, throw, or return.
  • Use default or _ to handle unmatched values.
  • Empty cases fall through; use break to prevent fallthrough in an empty case.
  • Use continue with a label for non-sequential fallthrough.
  • Use logical-or patterns (case a || b) to share a body between cases.

switch expressions

final color = switch (shape) {
  Circle() => 'red',
  Square() => 'blue',
  _ => 'unknown',
};
  • Omit case; use => for bodies; separate cases with commas.
  • Default must use _ (not default).
  • Produces a value.

Exhaustiveness

  • Dart checks exhaustiveness in switch statements and expressions at compile time.
  • Use default/_, enums, or sealed types to satisfy exhaustiveness.
sealed class Shape {}
class Circle extends Shape {}
class Square extends Shape {}

// Dart knows all subtypes — no default needed:
String describe(Shape s) => switch (s) {
  Circle() => 'circle',
  Square() => 'square',
};

Guard clauses

switch (point) {
  case (int x, int y) when x == y:
    print('Diagonal: $x');
  case (int x, int y):
    print('$x, $y');
}
  • Add when condition after a pattern to further constrain matching.
  • Usable in if-case, switch statements, and switch expressions.
  • If the guard is false, execution proceeds to the next case.

2. Patterns

Patterns represent the shape of a value for matching and destructuring.

Uses

// Variable declaration
var (a, [b, c]) = ('str', [1, 2]);

// Variable assignment (swap)
(b, a) = (a, b);

// for-in loop destructuring
for (final MapEntry(:key, :value) in map.entries) { ... }

// switch / if-case (see Branches section)
  • Wildcard _ ignores parts of a matched value.
  • Rest elements (...) in list patterns ignore remaining elements.
  • Case patterns are refutable: if no match, execution continues to the next case.
  • Destructured values in a case become local variables scoped to that case body.

Object patterns

var Foo(:one, :two) = myFoo;

JSON / nested data validation

if (data case {'user': [String name, int age]}) {
  print('$name, $age');
}

3. Pattern Types

Pattern Syntax Description
Logical-or p1 \|\| p2 Matches if any branch matches. All branches must bind the same variables.
Logical-and p1 && p2 Matches if both match. Variable names must not overlap.
Relational == c, < c, >= c Compares value to a constant. Combine with && for ranges.
Cast subpattern as Type Asserts type, then matches inner pattern. Throws if type mismatch.
Null-check subpattern? Matches non-null; binds non-nullable type.
Null-assert subpattern! Matches non-null or throws. Use in declarations to eliminate nulls.
Constant 42, 'str', const Foo() Matches if value equals the constant.
Variable var name, final Type name Binds matched value to a new variable. Typed form only matches the declared type.
Wildcard _, Type _ Matches any value without binding.
Parenthesized (subpattern) Controls precedence.
List [p1, p2] Matches lists by position. Length must match unless a rest element is used.
Rest element ..., ...rest Matches arbitrary-length tails or collects remaining elements.
Map {'key': subpattern} Matches maps by key. Missing keys throw StateError.
Record (p1, p2), (x: p1, y: p2) Matches records by shape; field names can be omitted if inferred.
Object ClassName(field: p) Matches by type and destructures via getters. Extra fields ignored.
  • Use parentheses to group lower-precedence patterns.
  • All pattern types can be nested and combined.

4. Records

// Create
var record = ('first', a: 2, b: true, 'last');

// Type annotation
({int a, bool b}) namedRecord;

// Access
print(record.$1);   // positional: 'first'
print(record.a);    // named: 2
  • Records are anonymous, immutable, fixed-size aggregates.
  • Each field can have a different type (heterogeneous).
  • Fields are accessed via built-in getters ($1, $2, .name); no setters.
  • Two records are equal if they have the same shape and equal field values.
  • hashCode and == are automatically defined.

Multiple return values

(String name, int age) userInfo(Map<String, dynamic> json) {
  return (json['name'] as String, json['age'] as int);
}

var (name, age) = userInfo(json);
// Named fields:
final (:name, :age) = userInfo(json);

Records vs. data classes

Use a record when:

  • Returning multiple values from a single function (small, one-time use).
  • Grouping a few values locally with no reuse across the codebase.
  • You need structural equality with no additional behavior.

Use a class when:

  • The type is reused across multiple files or features.
  • You need methods, encapsulation, inheritance, or copyWith.
  • The type is part of a public API or long-lived data model.
  • Changing the shape must be caught by the type system across the codebase.

Other best practices

  • Use typedef for record types to improve readability and maintainability.
  • Changing a record type alias does not guarantee type safety across the codebase — only classes provide full abstraction.

5. Migration Workflow

When modernizing pre-Dart-3 code, follow these steps:

Step 1 — Replace if-else chains with switch expressions

// Before (pre-Dart 3)
String label;
if (status == Status.loading) {
  label = 'Loading...';
} else if (status == Status.success) {
  label = 'Done';
} else {
  label = 'Error';
}

// After (Dart 3)
final label = switch (status) {
  Status.loading => 'Loading...',
  Status.success => 'Done',
  Status.error => 'Error',
};

Step 2 — Convert abstract class hierarchies to sealed classes

// Before
abstract class Result {}
class Success extends Result { final String data; Success(this.data); }
class Failure extends Result { final String error; Failure(this.error); }

// After — enables exhaustive switch
sealed class Result {}
final class Success extends Result { const Success(this.data); final String data; }
final class Failure extends Result { const Failure(this.error); final String error; }

Step 3 — Use destructuring for multiple return values

Replace wrapper classes used solely for returning multiple values with records.

Step 4 — Validate

Run dart analyze to confirm exhaustiveness and type safety after each change.


Files (flutter-ai-rules)
  • SKILL.md 7.9 KB
    ---
    name: dart-3-updates
    description: "Use when writing switch statements, refactoring if-else chains, creating data classes, choosing records vs classes, destructuring values, or modernizing pre-Dart-3 code."
    license: MIT
    ---
    
    # Dart 3 Updates Skill
    
    Apply Dart 3 language features — branches, patterns, pattern types, and records — correctly and idiomatically.
    
    ## When to Use
    
    Use this skill when:
    
    * Writing or refactoring `switch` statements or `if-else` chains.
    * Creating new data-holding classes and deciding between sealed classes, records, or plain classes.
    * Destructuring values from maps, lists, records, or objects.
    * Modernizing pre-Dart-3 code to use patterns, exhaustiveness checks, or switch expressions.
    
    ---
    
    ## 1. Branches
    
    ### if / if-case
    
    ```dart
    // Standard if
    if (score >= 90) {
      grade = 'A';
    } else if (score >= 80) {
      grade = 'B';
    } else {
      grade = 'C';
    }
    
    // if-case: match and destructure against a single pattern
    if (pair case [int x, int y]) {
      print('$x, $y');
    }
    ```
    
    - `if` conditions must evaluate to a `bool`.
    - In `if-case`, variables declared in the pattern are scoped to the matching branch.
    - If the pattern does not match, control flows to the `else` branch (if present).
    
    ### switch statements
    
    ```dart
    switch (command) {
      case 'quit':
        quit();
      case 'start' || 'begin': // logical-or pattern
        startGame();
      default:
        print('Unknown command');
    }
    ```
    
    - Each matched `case` body executes and jumps to the end — `break` is **not required**.
    - Non-empty cases can end with `continue`, `throw`, or `return`.
    - Use `default` or `_` to handle unmatched values.
    - Empty cases fall through; use `break` to prevent fallthrough in an empty case.
    - Use `continue` with a label for non-sequential fallthrough.
    - Use logical-or patterns (`case a || b`) to share a body between cases.
    
    ### switch expressions
    
    ```dart
    final color = switch (shape) {
      Circle() => 'red',
      Square() => 'blue',
      _ => 'unknown',
    };
    ```
    
    - Omit `case`; use `=>` for bodies; separate cases with commas.
    - Default must use `_` (not `default`).
    - Produces a value.
    
    ### Exhaustiveness
    
    - Dart checks exhaustiveness in `switch` statements and expressions at compile time.
    - Use `default`/`_`, enums, or `sealed` types to satisfy exhaustiveness.
    
    ```dart
    sealed class Shape {}
    class Circle extends Shape {}
    class Square extends Shape {}
    
    // Dart knows all subtypes — no default needed:
    String describe(Shape s) => switch (s) {
      Circle() => 'circle',
      Square() => 'square',
    };
    ```
    
    ### Guard clauses
    
    ```dart
    switch (point) {
      case (int x, int y) when x == y:
        print('Diagonal: $x');
      case (int x, int y):
        print('$x, $y');
    }
    ```
    
    - Add `when condition` after a pattern to further constrain matching.
    - Usable in `if-case`, `switch` statements, and `switch` expressions.
    - If the guard is `false`, execution proceeds to the next case.
    
    ---
    
    ## 2. Patterns
    
    Patterns represent the **shape** of a value for matching and destructuring.
    
    ### Uses
    
    ```dart
    // Variable declaration
    var (a, [b, c]) = ('str', [1, 2]);
    
    // Variable assignment (swap)
    (b, a) = (a, b);
    
    // for-in loop destructuring
    for (final MapEntry(:key, :value) in map.entries) { ... }
    
    // switch / if-case (see Branches section)
    ```
    
    - Wildcard `_` ignores parts of a matched value.
    - Rest elements (`...`) in list patterns ignore remaining elements.
    - Case patterns are **refutable**: if no match, execution continues to the next case.
    - Destructured values in a case become local variables scoped to that case body.
    
    ### Object patterns
    
    ```dart
    var Foo(:one, :two) = myFoo;
    ```
    
    ### JSON / nested data validation
    
    ```dart
    if (data case {'user': [String name, int age]}) {
      print('$name, $age');
    }
    ```
    
    ---
    
    ## 3. Pattern Types
    
    | Pattern | Syntax | Description |
    |---|---|---|
    | Logical-or | `p1 \|\| p2` | Matches if any branch matches. All branches must bind the same variables. |
    | Logical-and | `p1 && p2` | Matches if both match. Variable names must not overlap. |
    | Relational | `== c`, `< c`, `>= c` | Compares value to a constant. Combine with `&&` for ranges. |
    | Cast | `subpattern as Type` | Asserts type, then matches inner pattern. Throws if type mismatch. |
    | Null-check | `subpattern?` | Matches non-null; binds non-nullable type. |
    | Null-assert | `subpattern!` | Matches non-null or throws. Use in declarations to eliminate nulls. |
    | Constant | `42`, `'str'`, `const Foo()` | Matches if value equals the constant. |
    | Variable | `var name`, `final Type name` | Binds matched value to a new variable. Typed form only matches the declared type. |
    | Wildcard | `_`, `Type _` | Matches any value without binding. |
    | Parenthesized | `(subpattern)` | Controls precedence. |
    | List | `[p1, p2]` | Matches lists by position. Length must match unless a rest element is used. |
    | Rest element | `...`, `...rest` | Matches arbitrary-length tails or collects remaining elements. |
    | Map | `{'key': subpattern}` | Matches maps by key. Missing keys throw `StateError`. |
    | Record | `(p1, p2)`, `(x: p1, y: p2)` | Matches records by shape; field names can be omitted if inferred. |
    | Object | `ClassName(field: p)` | Matches by type and destructures via getters. Extra fields ignored. |
    
    - Use parentheses to group lower-precedence patterns.
    - All pattern types can be **nested and combined**.
    
    ---
    
    ## 4. Records
    
    ```dart
    // Create
    var record = ('first', a: 2, b: true, 'last');
    
    // Type annotation
    ({int a, bool b}) namedRecord;
    
    // Access
    print(record.$1);   // positional: 'first'
    print(record.a);    // named: 2
    ```
    
    - Records are **anonymous, immutable, fixed-size** aggregates.
    - Each field can have a different type (heterogeneous).
    - Fields are accessed via built-in getters (`$1`, `$2`, `.name`); no setters.
    - Two records are equal if they have the same shape and equal field values.
    - `hashCode` and `==` are automatically defined.
    
    ### Multiple return values
    
    ```dart
    (String name, int age) userInfo(Map<String, dynamic> json) {
      return (json['name'] as String, json['age'] as int);
    }
    
    var (name, age) = userInfo(json);
    // Named fields:
    final (:name, :age) = userInfo(json);
    ```
    
    ### Records vs. data classes
    
    Use a **record** when:
    - Returning multiple values from a single function (small, one-time use).
    - Grouping a few values locally with no reuse across the codebase.
    - You need structural equality with no additional behavior.
    
    Use a **class** when:
    - The type is reused across multiple files or features.
    - You need methods, encapsulation, inheritance, or `copyWith`.
    - The type is part of a public API or long-lived data model.
    - Changing the shape must be caught by the type system across the codebase.
    
    ### Other best practices
    
    - Use `typedef` for record types to improve readability and maintainability.
    - Changing a record type alias does not guarantee type safety across the codebase — only classes provide full abstraction.
    
    ---
    
    ## 5. Migration Workflow
    
    When modernizing pre-Dart-3 code, follow these steps:
    
    ### Step 1 — Replace if-else chains with switch expressions
    
    ```dart
    // Before (pre-Dart 3)
    String label;
    if (status == Status.loading) {
      label = 'Loading...';
    } else if (status == Status.success) {
      label = 'Done';
    } else {
      label = 'Error';
    }
    
    // After (Dart 3)
    final label = switch (status) {
      Status.loading => 'Loading...',
      Status.success => 'Done',
      Status.error => 'Error',
    };
    ```
    
    ### Step 2 — Convert abstract class hierarchies to sealed classes
    
    ```dart
    // Before
    abstract class Result {}
    class Success extends Result { final String data; Success(this.data); }
    class Failure extends Result { final String error; Failure(this.error); }
    
    // After — enables exhaustive switch
    sealed class Result {}
    final class Success extends Result { const Success(this.data); final String data; }
    final class Failure extends Result { const Failure(this.error); final String error; }
    ```
    
    ### Step 3 — Use destructuring for multiple return values
    
    Replace wrapper classes used solely for returning multiple values with records.
    
    ### Step 4 — Validate
    
    Run `dart analyze` to confirm exhaustiveness and type safety after each change.
    
    ---
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related