Dart Idioms skill for Codex
Sealed types, patterns and null safety without bangs. Codex reads the SDK constraint in pubspec.yaml and uses only the Dart features that language version allows. Domain states become sealed classes matched by exhaustive switch expressions, JSON is destructured with patterns, nullable values are handled at the boundary instead of with the bang operator, and every change passes dart analyze with strict casts and unawaited-future checks.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- dart-idioms Installs to ~/.agents/skills/dart-idioms/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install dart-idioms The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project dart-idioms Installs to .agents/skills/dart-idioms/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/dart-idioms.zipunzip -o dart-idioms.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) dart-idioms PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $dart-idioms, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes idiomatic Dart 3 - sound null safety without bang-operator sprinkling, records and destructuring, patterns and exhaustive switch expressions over sealed classes, class modifiers, extension types, async/await with Futures and Streams, isolates for CPU work - kept clean with dart analyze, dart fix, dart format and analysis_options.yaml lints. Use when writing or reviewing Dart packages, CLIs or the non-widget logic of a Flutter app, migrating pre-Dart-3 code, or fixing analyzer warnings and null-safety errors.
Use it when
- Writing Dart packages, CLIs or server code
- Cleaning up the business logic layer of a Flutter app
- Migrating older Dart code to Dart 3 records, patterns and class modifiers
- Null-safety errors or analyzer warnings need fixing
Use something else when
- The work is Flutter widgets, layout or platform channels: use flutter-apps
- You are choosing between Riverpod and Bloc: use flutter-state-management
How to check the result
dart analyze --fatal-infos and the format check pass, and tests cover each variant of any sealed type that changed.
The mistake it prevents
Adding a default arm to a switch over a sealed class, which silences the compiler error that would flag unhandled new variants.
Idiomatic Dart 3 code makes illegal states unrepresentable (sealed hierarchies,
exhaustive switches, non-nullable by default), uses ! only where a comment could
explain why the value cannot be null, and passes dart analyze with the project’s lint
set and zero warnings.
Before changing code
- Read
AGENTS.md,pubspec.yaml(theenvironment: sdk:lower bound sets the language version and therefore which features compile),pubspec.lockandanalysis_options.yaml. dart --version; for Flutter projects useflutter analyze/flutter test, which wrap the same tools.dart pub getneeds network access unless the pub cache is warm; ask first in a restricted sandbox.
Feature map
| Feature | Language version | Use for |
|---|---|---|
Records (int, String), ({int x, int y}) | 3.0 | returning several values without a class |
Patterns, switch expressions, if-case | 3.0 | destructuring JSON, exhaustive branching |
sealed, final, base, interface, mixin class modifiers | 3.0 | closed hierarchies and API control |
| Private final field promotion | 3.2 | if (_x != null) use(_x) works on private finals |
| Extension types | 3.3 | zero-cost wrappers (IDs, units) over existing types |
Digit separators 1_000_000 | 3.6 | readable literals |
Wildcard _ variables (non-binding) | 3.7 | ignored params (_, _) => 0 |
Null-aware elements [?maybe] | 3.8 | building lists and maps from nullable values |
Dot shorthands .active where the type is known | 3.10 | enum and static member shorthand |
Do not use a feature newer than the SDK lower bound in pubspec.yaml; raise the bound
deliberately (it also changes formatter style, see below).
Modelling with sealed classes and patterns
sealed class PaymentResult {}
final class Approved extends PaymentResult {
Approved(this.transactionId);
final String transactionId;
}
final class Declined extends PaymentResult {
Declined(this.reason, {this.retryable = false});
final String reason;
final bool retryable;
}
final class NeedsAction extends PaymentResult {
NeedsAction(this.url);
final Uri url;
}
String describe(PaymentResult r) => switch (r) {
Approved(:final transactionId) => 'Approved ($transactionId)',
Declined(retryable: true) => 'Declined, try again',
Declined(:final reason) => 'Declined: $reason',
NeedsAction(:final url) => 'Continue at $url',
};
Adding a subclass makes every non-exhaustive switch a compile error, which is the
point. Avoid a default/_ arm on sealed types; it silences that check.
Parsing JSON with patterns
User parseUser(Object? json) {
if (json case {'id': int id, 'name': String name, 'email': String? email}) {
return User(id: id, name: name, email: email);
}
throw FormatException('Invalid user payload: $json');
}
For large schemas, use code generation (json_serializable, freezed) that the
project already has instead of hand-written patterns.
Null safety without !
| Instead of | Prefer |
|---|---|
user!.name after a separate null check on a field | copy to a local: final u = user; if (u != null) u.name (public fields do not promote) |
late for values that may never be set | nullable type and handle null, or set it in the constructor |
map[key]! | map[key] ?? fallback, or if (map[key] case final v?) |
list.firstWhere(..., orElse: () => null) (does not compile) | list.firstWhereOrNull from package:collection, or where(...).firstOrNull |
String? name passed through five layers | validate at the boundary and use non-nullable types inside |
late final is fine for values initialised exactly once before use (e.g. in
initState); reading too early throws LateInitializationError at runtime.
Records and extension types
(double lat, double lng) parseCoord(String s) {
final [a, b] = s.split(',');
return (double.parse(a), double.parse(b));
}
final (lat, lng) = parseCoord('51.5,-0.12');
extension type const UserId(String value) {
UserId.parse(String raw) : value = raw.trim() {
if (!value.startsWith('usr_')) throw FormatException('bad id: $raw');
}
}
The list pattern final [a, b] = ... throws if the split does not produce exactly two
parts; that is a feature at a parsing boundary. Extension types vanish at runtime
(is checks see the underlying type), so they document intent but do not secure it.
Async
- Every
Futureis awaited, returned, or explicitlyunawaited(...)(fromdart:async); enable theunawaited_futureslint. - Parallel independent work:
final (a, b) = await (fetchA(), fetchB()).wait;(record.wait, 3.0+) orFuture.wait. - Timeouts:
await call().timeout(const Duration(seconds: 5)), and handleTimeoutException. - Streams: cancel subscriptions you start (
StreamSubscription.cancel()indispose); preferawait forin non-UI code. - CPU-bound work blocks the event loop; use
Isolate.run(() => heavyParse(bytes))(Flutter:compute). Isolates do not share memory; pass and return plain data.
Lints and formatting
# analysis_options.yaml
include: package:lints/recommended.yaml # Flutter: package:flutter_lints/flutter.yaml
analyzer:
language:
strict-casts: true
strict-inference: true
strict-raw-types: true
errors:
unawaited_futures: error
linter:
rules:
- unawaited_futures
- prefer_final_locals
- avoid_dynamic_calls
dart analyze --fatal-infos # zero issues is the bar
dart fix --dry-run # preview automated fixes, then: dart fix --apply
dart format . # style follows the package's language version
dart test # or flutter test
Since Dart 3.7 the formatter uses the “tall” style for code at language version 3.7+. Raising the SDK lower bound can reformat the whole package; do that in its own commit.
Gotchas
==on classes is identity unless overridden; records compare structurally. For data classes, override==/hashCodetogether or use codegen.constconstructors make canonical instances;const []lists are unmodifiable and throw onadd.dynamicturns off checking; preferObject?and pattern-match it.intis 64-bit on the VM but follows JS number semantics on the web: large IDs lose precision when compiled to JS; keep them asString.- Top-level and static variables are lazily initialised; side effects in their initialisers run on first access, not at startup.
catch (e)catches everything includingErrors (programming bugs); catch specific exceptions withon FormatException catch (e).
Verification
-
dart analyze --fatal-infos(orflutter analyze) is clean. -
dart format --output=none --set-exit-if-changed .passes. - Tests pass; sealed-type changes have a test per variant.
- Report the SDK constraint and any language features introduced.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Dart Idioms"
short_description: "Sealed types, patterns and null safety without bangs"
default_prompt: "$dart-idioms Refactor these Dart result classes into a sealed class with switch expressions" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.