Files
everything-claude-code/rules/dart/coding-style.md
2026-04-02 17:48:43 -07:00

160 lines
4.4 KiB
Markdown

---
paths:
- "**/*.dart"
- "**/pubspec.yaml"
- "**/analysis_options.yaml"
---
# Dart/Flutter Coding Style
> This file extends [common/coding-style.md](../common/coding-style.md) with Dart and Flutter-specific content.
## Formatting
- **dart format** for all `.dart` files — enforced in CI (`dart format --set-exit-if-changed .`)
- Line length: 80 characters (dart format default)
- Trailing commas on multi-line argument/parameter lists to improve diffs and formatting
## Immutability
- Prefer `final` for local variables and `const` for compile-time constants
- Use `const` constructors wherever all fields are `final`
- Return unmodifiable collections from public APIs (`List.unmodifiable`, `Map.unmodifiable`)
- Use `copyWith()` for state mutations in immutable state classes
```dart
// BAD
var count = 0;
List<String> items = ['a', 'b'];
// GOOD
final count = 0;
const items = ['a', 'b'];
```
## Naming
Follow Dart conventions:
- `camelCase` for variables, parameters, and named constructors
- `PascalCase` for classes, enums, typedefs, and extensions
- `snake_case` for file names and library names
- `SCREAMING_SNAKE_CASE` for constants declared with `const` at top level
- Prefix private members with `_`
- Extension names describe the type they extend: `StringExtensions`, not `MyHelpers`
## Null Safety
- Avoid `!` (bang operator) — prefer `?.`, `??`, `if (x != null)`, or Dart 3 pattern matching; reserve `!` only where a null value is a programming error and crashing is the right behaviour
- Avoid `late` unless initialization is guaranteed before first use (prefer nullable or constructor init)
- Use `required` for constructor parameters that must always be provided
```dart
// BAD — crashes at runtime if user is null
final name = user!.name;
// GOOD — null-aware operators
final name = user?.name ?? 'Unknown';
// GOOD — Dart 3 pattern matching (exhaustive, compiler-checked)
final name = switch (user) {
User(:final name) => name,
null => 'Unknown',
};
// GOOD — early-return null guard
String getUserName(User? user) {
if (user == null) return 'Unknown';
return user.name; // promoted to non-null after the guard
}
```
## Sealed Types and Pattern Matching (Dart 3+)
Use sealed classes to model closed state hierarchies:
```dart
sealed class AsyncState<T> {
const AsyncState();
}
final class Loading<T> extends AsyncState<T> {
const Loading();
}
final class Success<T> extends AsyncState<T> {
const Success(this.data);
final T data;
}
final class Failure<T> extends AsyncState<T> {
const Failure(this.error);
final Object error;
}
```
Always use exhaustive `switch` with sealed types — no default/wildcard:
```dart
// BAD
if (state is Loading) { ... }
// GOOD
return switch (state) {
Loading() => const CircularProgressIndicator(),
Success(:final data) => DataWidget(data),
Failure(:final error) => ErrorWidget(error.toString()),
};
```
## Error Handling
- Specify exception types in `on` clauses — never use bare `catch (e)`
- Never catch `Error` subtypes — they indicate programming bugs
- Use `Result`-style types or sealed classes for recoverable errors
- Avoid using exceptions for control flow
```dart
// BAD
try {
await fetchUser();
} catch (e) {
log(e.toString());
}
// GOOD
try {
await fetchUser();
} on NetworkException catch (e) {
log('Network error: ${e.message}');
} on NotFoundException {
handleNotFound();
}
```
## Async / Futures
- Always `await` Futures or explicitly call `unawaited()` to signal intentional fire-and-forget
- Never mark a function `async` if it never `await`s anything
- Use `Future.wait` / `Future.any` for concurrent operations
- Check `context.mounted` before using `BuildContext` after any `await` (Flutter 3.7+)
```dart
// BAD — ignoring Future
fetchData(); // fire-and-forget without marking intent
// GOOD
unawaited(fetchData()); // explicit fire-and-forget
await fetchData(); // or properly awaited
```
## Imports
- Use `package:` imports throughout — never relative imports (`../`) for cross-feature or cross-layer code
- Order: `dart:` → external `package:` → internal `package:` (same package)
- No unused imports — `dart analyze` enforces this with `unused_import`
## Code Generation
- Generated files (`.g.dart`, `.freezed.dart`, `.gr.dart`) must be committed or gitignored consistently — pick one strategy per project
- Never manually edit generated files
- Keep generator annotations (`@JsonSerializable`, `@freezed`, `@riverpod`, etc.) on the canonical source file only