Skip to content

code conventions

style rules for lib/ and test/ that are not covered by the architecture, design-system, or terminology docs. some are enforced by analysis_options.yaml; the rest are convention — follow them anyway so the codebase stays uniform.

naming

  • use descriptive names; do not abbreviate. the only accepted abbreviations are id, url, csv, json, ui, db, l10n (the canonical local name for AppLocalizations), tts (matches the plugin names; see terminology.md), and standard import aliases (p, tz, math).
  • spell out units and roles: milliseconds not ms, directory not dir, extension/fileExtension not ext, silencePhone not dnd, repository not repo.
  • catch clauses bind error and stackTrace (or catch (_) when the values are unused) — never e/st/ex.
  • loop counters and closure parameters get descriptive names (index, rowIndex, stepIndex, segment, sound) — no bare i, j, e, or t. drift query lambdas name the row parameter row (..where((row) => row.name.equals(name))).
  • Dart casing conventions apply everywhere: lowerCamelCase for variables, parameters, methods, and fields; UpperCamelCase for classes, enums, and typedefs; snake_case for file names. never use snake_case for Dart locals — language convention wins over personal preference.
  • terminology.md governs every identifier that maps to a user-facing concept, and persisted names (tables, settings keys, wire values, sync ids) are frozen — check it before naming anything new.

comments

  • comment liberally. every public class and every public member gets a /// doc comment saying what it is and anything a caller must know; members marked @override inherit their interface's docs and only need one when the override changes the contract.
  • inside a body, // comments explain why, not what — the code already says what. prefer one well-placed paragraph over narrating every line.
  • comments follow the same lowercase-prose convention as documentation: lowercase sentences, with proper nouns (Wear OS, GitLab) and code identifiers (has_seen_welcome, AppDatabase) keeping canonical casing.

ordering

  • prefer alphabetic ordering when no clearer order exists — lists, map literals, switch cases, enum values, and class members.
  • a clearer order beats alphabetic: temporal sequences (BreathPhase is inhale → hold → exhale → second hold), grouping by kind (HapticPattern lists one-shot patterns before continuous ones), read/save pairs (SettingsRepository keeps each getter next to its setter), and feature blocks. when the chosen order is not obvious, a short comment should say what it is.
  • pubspec.yaml keeps the flutter:/flutter_test: sdk entries first, then lists the rest alphabetically. sort_pub_dependencies stays off for this reason — see analysis_options.yaml.
  • lib/l10n/app_en.arb keys are alphabetical; shared prefixes group related strings naturally. each key's @key metadata entry stays directly beneath it.
  • imports follow the directives_ordering lint: dart:, then package:, then relative, alphabetical inside each group.

testing

  • every behavior change ships with tests — test/ mirrors lib/, platform plugins are faked through their data/services/ interface with mocktail, and the l10n guard tests keep strings honest. when the service under test is the plugin wrapper, fake one layer lower at the plugin's platform interface instead — VibrationPlatform.instance for haptics — so the wrapper's own logic stays in the test.
  • audio has a second tier above the mocked-engine tests: the real flutter_soloud engine runs headless under flutter test (a dart native asset; with no playback device it uses its null backend, which still mixes real pcm). test/helpers/real_soloud.dart initializes the singleton once per test file and RenderedAudio taps audioVisualizationEvents, so a test asserts on what the engine actually rendered — audible, silent, the tone on each channel — rather than on the calls made to it. keep real-time windows short; long schedules and session-length fades belong to the simulated-engine tests. hosts that cannot init the engine skip via hasRealEngine; hosts with a real soundcard will audibly play the test sounds.
  • new code gets coverage where practical; run flutter test before considering a change done.
  • the suite passes on any development host, not only in the linux ci image. code that branches on the operating system takes it as a constructor argument that defaults to the host (see FlutterLocalNotificationsService), so a test picks the path it exercises instead of inheriting the machine's.
  • golden baselines under test/golden/goldens/ are rendered on linux. test/golden/flutter_test_config.dart compares them exactly on linux and within 1% of pixels on other hosts, which absorbs font and anti-aliasing differences (up to 0.5% on macos) without hiding a real change. regenerate baselines on linux — the ci image — with flutter test --update-goldens, never from another host.

documentation

  • a code change updates docs in the same change: docs/dev-guide/ for how the code works, the affected docs/user-guide/ page for what the user sees, and CHANGELOG.md under the topmost ## 0.0.x (unreleased) section for anything user-visible or packaging-related.
  • user-facing English strings go through lib/l10n/app_en.arb only — never hardcode UI text.

consistency

  • follow the surrounding convention over introducing a new one: relative imports inside lib/, package: imports inside test/, riverpod providers via @riverpod/@Riverpod codegen, and generated files (*.g.dart, *.freezed.dart, *.drift.dart, lib/l10n/generated/) are never hand-edited.