rules/dart/coding-style.md
This file extends common/coding-style.md with Dart and Flutter-specific content.
.dart files — enforced in CI (dart format --set-exit-if-changed .)final for local variables and const for compile-time constantsconst constructors wherever all fields are finalList.unmodifiable, Map.unmodifiable)copyWith() for state mutations in immutable state classes// BAD
var count = 0;
List<String> items = ['a', 'b'];
// GOOD
final count = 0;
const items = ['a', 'b'];
Follow Dart conventions:
camelCase for variables, parameters, and named constructorsPascalCase for classes, enums, typedefs, and extensionssnake_case for file names and library namesSCREAMING_SNAKE_CASE for constants declared with const at top level_StringExtensions, not MyHelpers! (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 behaviourlate unless initialization is guaranteed before first use (prefer nullable or constructor init)required for constructor parameters that must always be provided// 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
}
Use sealed classes to model closed state hierarchies:
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:
// BAD
if (state is Loading) { ... }
// GOOD
return switch (state) {
Loading() => const CircularProgressIndicator(),
Success(:final data) => DataWidget(data),
Failure(:final error) => ErrorWidget(error.toString()),
};
on clauses — never use bare catch (e)Error subtypes — they indicate programming bugsResult-style types or sealed classes for recoverable errors// 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();
}
await Futures or explicitly call unawaited() to signal intentional fire-and-forgetasync if it never awaits anythingFuture.wait / Future.any for concurrent operationscontext.mounted before using BuildContext after any await (Flutter 3.7+)// BAD — ignoring Future
fetchData(); // fire-and-forget without marking intent
// GOOD
unawaited(fetchData()); // explicit fire-and-forget
await fetchData(); // or properly awaited
package: imports throughout — never relative imports (../) for cross-feature or cross-layer codedart: → external package: → internal package: (same package)dart analyze enforces this with unused_import.g.dart, .freezed.dart, .gr.dart) must be committed or gitignored consistently — pick one strategy per project@JsonSerializable, @freezed, @riverpod, etc.) on the canonical source file only