-
Notifications
You must be signed in to change notification settings - Fork 0
Dev Dart Best Practices
Project-specific conventions on top of Effective Dart. Lint is enforced via analysis_options.yaml.
All files are sound null-safe. No ! escape hatches unless unavoidable — prefer:
// prefer
final name = station?.name ?? l10n.unknownStation;
// over
final name = station!.name;Use late only for lifecycle-bound fields that are guaranteed to be set before first read (e.g. late final Obd2Service _service; after connect()).
Every model is @freezed:
@freezed
class Station with _$Station {
const factory Station({
required String id,
required String brand,
required double latitude,
required double longitude,
@Default([]) List<FuelPrice> prices,
}) = _Station;
factory Station.fromJson(Map<String, dynamic> json) => _$StationFromJson(json);
}After editing a freezed file, run:
dart run build_runner build --delete-conflicting-outputsMutating an existing instance:
final updated = station.copyWith(prices: [...station.prices, newPrice]);-
async/awaiteverywhere. ExplicitFuture<T>return types. -
unawaited(...)when fire-and-forget is intentional (e.g.unawaited(traceRecorder.record(e, st));). - Never
Future.value(null)as a placeholder —Future.value()or useSynchronousFutureif truly sync. - Don't forget
await— theunawaited_futureslint catches most of it.
Prefer Stream<T> over StreamController<T> exposed to callers:
Stream<Position> positionStream() async* {
await for (final pos in _raw.positionStream) yield pos;
}Great for adding domain-specific helpers without inheritance:
extension FuelPriceX on FuelPrice {
bool get isStale => DateTime.now().difference(fetchedAt) > Duration(minutes: 5);
String get formatted => UnitFormatter.formatPricePerUnit(eurPerLiter);
}Keep extensions in the same file as the type, or in *_x.dart files co-located with the type.
-
AppException/ApiExceptionhierarchy withmessage+ optionalstatusCode - Never
catch (_) {}(see the pinning test intest/lint/) - Always pass the stack trace to the trace recorder
try {
await something();
} on ApiException catch (e, st) {
debugPrint('API failure: $e');
traceRecorder.record(e, st);
rethrow;
}See Error Reporting & Tracing.
Anywhere you find a magic string or number that could be wrong, pin it:
class CacheTtl {
static const stationSearch = Duration(minutes: 5);
static const stationDetail = Duration(minutes: 15);
static const geocode = Duration(hours: 24);
// ...
}Pinning tests (test/core/constants/) guarantee no one silently changes them.
Prefer enhanced enums over string enums:
enum FuelType {
e5(label: 'E5', grade: 95),
e10(label: 'E10', grade: 95),
diesel(label: 'Diesel', grade: null),
;
const FuelType({required this.label, required this.grade});
final String label;
final int? grade;
}- Types:
UpperCamelCase—StationServiceChain,PricePrediction - Members / locals:
lowerCamelCase—fetchedAt,isStale - Private: leading
_—_cache,_inFlight - Constants: either
UpperCamelCasefor class-level, orlowerCamelCasefor compile-timeconst. Project convention: preferlowerCamelCase constinside classes (static const defaultRadius = 5). - Files:
snake_case.dart—station_service_chain.dart,obd2_connection_service.dart
Split into sub-widgets. presentation/widgets/ is where the pieces live. This keeps setState scopes small and widget tests focused.
Extract business logic to a service or repository, have the provider only orchestrate.
Always @freezed classes or Equatable. Never override operator == by hand unless the class is performance-critical.
Generated via json_serializable. Manual parsing only for external formats that freezed can't handle (e.g. OBD2 byte stream — that's why Obd2Service has hand-rolled parsers).
Only when the why isn't obvious:
// BAD
/// Returns the price.
double get price => _price;
// GOOD
// MAF-derived fuel rate: assumes gasoline stoichiometry (14.7:1 AFR, 820 g/L).
// Diesel approximation uses 14.5:1 — 3% high, close enough for trip summaries.
double _deriveFuelRateFromMaf(double mafGps, FuelType type) { ... }Perf win, and lint-enforced:
return const Center(child: CircularProgressIndicator());At last count the codebase had 1141+ const occurrences.
- Don't use
print— usedebugPrint(compiles away in release). - Don't instantiate
Dio()per request — useDioFactory.create(). - Don't use
setStatefor shared state — use a Riverpod provider. - Don't reach into
data/frompresentation/— go throughproviders/. - Don't hardcode strings — everything user-facing goes through ARB.
- Don't assume
contextis valid afterawait—if (!context.mounted) return; - Don't
catch (_) { }.
- Project Structure — where each file shape fits
- State Management (Riverpod) — the Riverpod-specific rules
- Testing & TDD — how these rules are enforced
👤 User Guide
🛠️ Developer Guide
Architecture
Code patterns
Quality
Deep dives
Reference
Workflow