
Dart 3 Updates
- 39 installs
- 605 repo stars
- Updated August 4, 2026
- evanca/flutter-ai-rules
Helps with ai & agent building tasks.
About
dart-3-updates is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- dart-3-updates
- AI & Agent Building
- AI-coding skill
Dart 3 Updates by the numbers
- 39 all-time installs (skills.sh)
- Ranked #8,302 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/evanca/flutter-ai-rules --skill dart-3-updatesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 39 |
|---|---|
| repo stars | ★ 605 |
| Last updated | August 4, 2026 |
| Repository | evanca/flutter-ai-rules ↗ |
What it does
Helps with ai & agent building tasks.
Files
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
switchstatements orif-elsechains. - 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');
}ifconditions must evaluate to abool.- In
if-case, variables declared in the pattern are scoped to the matching branch. - If the pattern does not match, control flows to the
elsebranch (if present).
switch statements
switch (command) {
case 'quit':
quit();
case 'start' || 'begin': // logical-or pattern
startGame();
default:
print('Unknown command');
}- Each matched
casebody executes and jumps to the end —breakis not required. - Non-empty cases can end with
continue,throw, orreturn. - Use
defaultor_to handle unmatched values. - Empty cases fall through; use
breakto prevent fallthrough in an empty case. - Use
continuewith 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
_(notdefault). - Produces a value.
Exhaustiveness
- Dart checks exhaustiveness in
switchstatements and expressions at compile time. - Use
default/_, enums, orsealedtypes 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 conditionafter a pattern to further constrain matching. - Usable in
if-case,switchstatements, andswitchexpressions. - 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 \ | \ |
| 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.
hashCodeand==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
typedeffor 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.
---