Skip to main content

loom-dart

Dart language expertise for idiomatic, production-quality code.

Source facts

Repository
cosmix/loom
Last source activity
September 24, 2026 at 17:08
Detected SKILL.md language
English
Stars
61
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
loom-dart
description
Dart language expertise for idiomatic, production-quality code.
triggers
["dart","flutter","pub","pubspec","pub.dev","dart test","flutter test","flutter_test","package:test","null safety","isolate","statefulwidget","statelesswidget","riverpod","provider","bloc","build_runner","freezed","json_serializable","mocktail","dart analyze","dart format"]
# Dart Language Expertise ## Overview Idiomatic, production-grade Dart 3 and Flutter: pub packages, sound null safety, records and patterns, futures and streams, isolates, and the widget lifecycle. Assumes the reader knows the syntax. The value is in what is easy to get wrong: unawaited futures, public fields that never promote, `BuildContext` used after an `await`, futures created in `build`, isolate message copying, and a test filter that matches by substring. ## Tooling `dart` (or `flutter` in a Flutter project) is the build tool, package manager, formatter, analyzer and test runner in one. | Task | Command | | --- | --- | | New package | `dart create -t package spool` / `flutter create --template=package spool` / `flutter create app` | | Add / remove dep | `dart pub add path` / `dart pub remove path` (`flutter pub add` in Flutter projects) | | Add dev dep | `dart pub add dev:test` | | Resolve | `dart pub get` (`--enforce-lockfile` in CI fails on drift from `pubspec.lock`) | | Outdated / upgrade | `dart pub outdated` / `dart pub upgrade --major-versions` | | Format | `dart format .` | | Analyze | `dart analyze --fatal-infos` / `flutter analyze` | | Apply fixes | `dart fix --apply` | | Code generation | `dart run build_runner build --delete-conflicting-outputs` | | Test | `dart test` / `flutter test` | | Native binary | `dart compile exe bin/spool.dart -o spool` | - Use `dart pub add` so `pubspec.yaml` and `pubspec.lock` stay in step. Commit `pubspec.lock` for applications. - Monorepos (3.6+): a root `pubspec.yaml` lists `workspace: [packages/a, packages/b]` and each member declares `resolution: workspace`, giving one shared resolution and one lockfile. - Generated files (`*.g.dart`, `*.freezed.dart`) come from `build_runner`; never edit them by hand. ```yaml # analysis_options.yaml include: package:lints/recommended.yaml # Flutter: package:flutter_lints/flutter.yaml analyzer: language: strict-casts: true # no implicit downcast from dynamic strict-inference: true strict-raw-types: true errors: unawaited_futures: error linter: rules: - unawaited_futures - discarded_futures - cancel_subscriptions - close_sinks - avoid_print ``` The canonical gate (swap in `flutter analyze` and `flutter test` for Flutter packages): ```bash dart format --output=none --set-exit-if-changed . dart analyze --fatal-infos dart test ``` ## Sound Null Safety - Types are non-nullable unless marked `T?`, and the guarantee is sound: a non-nullable static type never holds `null` at runtime. - Type promotion after a null check applies to local variables and parameters, and (3.2+) to private `final` fields. Public fields and getters never promote, because a subclass or another library could override them. Copy to a local first. - `!` throws `Null check operator used on a null value` at runtime. Each `!` is an unchecked claim; prefer promotion, `??`, or an early return. - `late` defers initialization. Reading a `late` field before it is written throws `LateInitializationError`. `late final x = compute();` runs `compute` lazily on first read. - Mark mandatory named parameters `required`. A nullable parameter checked inside the function moves the error from compile time to runtime. ```dart class Spool { Spool(this.path, {this.maxBytes}); final String path; final int? maxBytes; // public: never promotes bool isFull(int written) { final limit = maxBytes; // a local promotes if (limit == null) return false; return written >= limit; // limit is int here } } ``` ## Types, Records and Patterns - `sealed` classes make `switch` exhaustive: adding a subtype turns every non-exhaustive switch into a compile error. - Class modifiers state intent: `final` (no subtypes outside the library), `base` (inherit only), `interface` (implement only), `mixin class`. - Records return several values without a class: `(int, String)`, `({int line, int column})`. Records compare by value. - Patterns validate and destructure untyped data such as decoded JSON in one step. - Extension types (3.3) wrap a representation type at zero runtime cost, useful for ids and units. - Override `==` and `hashCode` together, or use a record or a generated data class. ```dart sealed class SpoolEvent {} final class Appended extends SpoolEvent { Appended(this.bytes); final int bytes; } final class Rotated extends SpoolEvent { Rotated(this.segment); final String segment; } String describe(SpoolEvent event) => switch (event) { Appended(:final bytes) => 'appended $bytes bytes', Rotated(:final segment) => 'rotated to $segment', }; extension type SegmentId(int value) {} Config parseConfig(Object? json) { if (json case {'path': String path, 'maxBytes': int maxBytes}) { return Config(path, maxBytes); } throw FormatException('invalid config', json); } ``` ## Errors - `Exception` types describe expected failures that callers handle. `Error` types (`StateError`, `ArgumentError`, `RangeError`) describe programming bugs; do not catch them in normal flow. - Catch by type with `on FormatException catch (e, st)`. A bare `catch (e)` also catches `Error`s; keep it to top-level handlers that log and report. - `rethrow` keeps the original stack trace. When translating an error, use `Error.throwWithStackTrace(SpoolException(...), st)`. - An error thrown inside an `async` function completes its `Future` with that error; nobody sees it unless the future is awaited or handled. - In Flutter, route uncaught errors to reporting through `FlutterError.onError` and `PlatformDispatcher.instance.onError`. ```dart final class SpoolException implements Exception { SpoolException(this.message, {this.path}); final String message; final String? path; @override String toString() => 'SpoolException: $message${path == null ? '' : ' ($path)'}'; } Future<Config> loadConfig(File file) async { try { return parseConfig(jsonDecode(await file.readAsString())); } on FormatException catch (e, st) { Error.throwWithStackTrace(SpoolException(e.message, path: file.path), st); } } ``` ## Async and Streams - Await every `Future`, or hand it to `unawaited(...)` (from `dart:async`) so the intent is visible. The `unawaited_futures` and `discarded_futures` lints enforce this. - `(a, b).wait` on a record of futures (3.0+) runs them concurrently and throws a `ParallelWaitError` carrying every result and error. `Future.wait` reports only the first error. - `future.timeout(d)` stops waiting; the underlying work keeps running. - Streams are single-subscription by default; a second `listen` throws `Bad state: Stream has already been listened to.` Use `StreamController.broadcast()` for multiple listeners. - Cancel every `StreamSubscription` and close every `StreamController` you create (`cancel_subscriptions`, `close_sinks`). - An `async*` generator pauses at `yield` while its subscriber is paused, which gives backpressure for free. ```dart Future<Dashboard> load(UserId id) async { final (profile, orders) = await (api.profile(id), api.orders(id)).wait; return Dashboard(profile, orders); } Stream<Segment> segments(Directory dir) async* { await for (final entity in dir.list()) { if (entity is File && entity.path.endsWith('.seg')) { yield await Segment.open(entity); } } } final subscription = watcher.events.listen(onEvent, onError: onError); // ... await subscription.cancel(); // otherwise the listener and its resources stay alive ``` ## Isolates - Each isolate has its own heap and event loop; there is no shared mutable memory. Messages are copied, except `TransferableTypedData` and the value returned through `Isolate.exit`. - `Isolate.run(() => work())` (2.19+) runs one computation on a new isolate and returns its result; use it for parsing or crunching that would stall the UI or a server's event loop. - The closure sent to an isolate carries everything it captures. Capturing `this` copies the whole object graph, and an unsendable member (a `Socket`, a `ReceivePort`, a native handle) throws `Illegal argument in isolate message`. Capture only plain data. - Long-lived workers: `Isolate.spawn` with a `SendPort`/`ReceivePort` pair. Flutter's `compute()` wraps `Isolate.run`; on the web there are no isolates and `compute()` runs on the main thread. ```dart Future<List<Entry>> parseLarge(Uint8List bytes) => Isolate.run(() => decodeEntries(bytes)); // bytes are copied into the new isolate ``` ## Flutter Essentials - Widgets are immutable configuration. `const` constructors let Flutter skip rebuilding identical subtrees. - `setState` wraps a synchronous mutation only. Do async work first, then call `setState` with the result. - After any `await` in a `State` method, check `mounted` (or `context.mounted` outside a `State`) before touching `context` or calling `setState`. The `use_build_context_synchronously` lint flags misses. - Create futures and streams in `initState`, never in `build`: `build` runs often, and a new future restarts `FutureBuilder` each time. - Dispose controllers, focus nodes, animation controllers and subscriptions in `dispose`. - Give list items that can move a `ValueKey(item.id)` so their state follows the item. - State management: `ValueNotifier`/`ChangeNotifier` with `ListenableBuilder` covers small apps. Follow the project's existing Provider, Riverpod or Bloc setup; do not mix them. - Split large `build` methods into small widget classes; helper methods returning widgets rebuild with their parent and cannot be `const`. ```dart class SpoolPage extends StatefulWidget { const SpoolPage({super.key, required this.client}); final SpoolClient client; @override State<SpoolPage> createState() => _SpoolPageState(); } class _SpoolPageState extends State<SpoolPage> { late final Future<List<String>> _entries; @override void initState() { super.initState(); _entries = widget.client.list(); // runs once per State } Future<void> _purge() async { await widget.client.purge(); if (!mounted) return; // the page may be gone after the await ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Purged'))); } @override Widget build(BuildContext context) { return FutureBuilder<List<String>>( future: _entries, builder: (context, snapshot) { if (snapshot.hasError) return Text('Error: ${snapshot.error}'); final entries = snapshot.data; if (entries == null) return const CircularProgressIndicator(); return ListView.builder( itemCount: entries.length, itemBuilder: (context, i) => ListTile(title: Text(entries[i]), onTap: _purge), ); }, ); } } ``` ## Testing `package:test` provides `test`, `group`, `setUp`, `tearDown`, `addTearDown` and the matcher library; `flutter_test` re-exports it and adds `testWidgets` with a `WidgetTester`. ```dart import 'package:mocktail/mocktail.dart'; import 'package:spool/spool.dart'; import 'package:test/test.dart'; class MockClient extends Mock implements SpoolClient {} void main() { group('Spool', () { late MockClient client; setUp(() => client = MockClient()); test('rotates when the limit is reached', () async { when(() => client.rotate()).thenAnswer((_) async {}); final spool = Spool(client, maxBytes: 4); await spool.append([1, 2, 3, 4, 5]); expect(spool.segments, hasLength(2)); verify(() => client.rotate()).called(1); }); test('rejects an empty path', () { expect(() => Spool.open(''), throwsA(isA<ArgumentError>())); }); test('emits appended then rotated', () async { when(() => client.rotate()).thenAnswer((_) async {}); final spool = Spool(client, maxBytes: 1); final events = expectLater(spool.events, emitsInOrder([isA<Appended>(), isA<Rotated>()])); await spool.append([1, 2]); await events; }); }); } ``` ```dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { testWidgets('purge shows a confirmation', (tester) async { await tester.pumpWidget(MaterialApp(home: Scaffold(body: SpoolPage(client: FakeClient())))); await tester.pump(); // resolve the entries future await tester.tap(find.text('first')); await tester.pump(); expect(find.text('Purged'), findsOneWidget); }); } ``` - Async tests return their `Future`; the runner waits for it. `expectLater` returns a future to await when the assertion itself is asynchronous. - `pumpAndSettle` pumps until no frames are scheduled and times out on endless animations such as a spinning `CircularProgressIndicator`; use `pump(duration)` there. - Control time with `package:fake_async` or an injected `Clock` from `package:clock`. - Golden tests (`matchesGoldenFile`, `flutter test --update-goldens`) render fonts differently across operating systems; generate and compare them on one platform. - Useful flags: `--name` (regex), `--plain-name` (substring), `-t`/`-x` for tags, `-p chrome`, `--reporter expanded`, `--coverage=coverage` (`--coverage` for Flutter). ## Loom Test Runner Adapter **Adapters.** `dart-test` and `flutter-test`. A directory containing `pubspec.yaml` is a `dart` package; loom picks `flutter-test` when `pubspec.yaml` depends on `flutter`, else `dart-test`. `loom project detect` prints the kind, runner and skill per package; confirm that a pure Dart package reports `dart-test`, since `flutter test` needs the Flutter SDK. **Single-test command.** Loom runs each contract with the package directory as the working directory: ```bash dart test {file} --plain-name '{test}' flutter test {file} --plain-name '{test}' ```
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub