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 forAppLocalizations),tts(matches the plugin names; seeterminology.md), and standard import aliases (p,tz,math). - spell out units and roles:
millisecondsnotms,directorynotdir,extension/fileExtensionnotext,silencePhonenotdnd,repositorynotrepo. - catch clauses bind
errorandstackTrace(orcatch (_)when the values are unused) — nevere/st/ex. - loop counters and closure parameters get descriptive names (
index,rowIndex,stepIndex,segment,sound) — no barei,j,e, ort. drift query lambdas name the row parameterrow(..where((row) => row.name.equals(name))). - Dart casing conventions apply everywhere:
lowerCamelCasefor variables, parameters, methods, and fields;UpperCamelCasefor classes, enums, and typedefs;snake_casefor file names. never usesnake_casefor Dart locals — language convention wins over personal preference. terminology.mdgoverns 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@overrideinherit 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 (
BreathPhaseis inhale → hold → exhale → second hold), grouping by kind (HapticPatternlists one-shot patterns before continuous ones), read/save pairs (SettingsRepositorykeeps 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.yamlkeeps theflutter:/flutter_test:sdk entries first, then lists the rest alphabetically.sort_pub_dependenciesstays off for this reason — seeanalysis_options.yaml.lib/l10n/app_en.arbkeys are alphabetical; shared prefixes group related strings naturally. each key's@keymetadata entry stays directly beneath it.- imports follow the
directives_orderinglint:dart:, thenpackage:, then relative, alphabetical inside each group.
testing
- every behavior change ships with tests —
test/mirrorslib/, platform plugins are faked through theirdata/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.instancefor haptics — so the wrapper's own logic stays in the test. - audio has a second tier above the mocked-engine tests: the real
flutter_soloudengine runs headless underflutter test(a dart native asset; with no playback device it uses its null backend, which still mixes real pcm).test/helpers/real_soloud.dartinitializes the singleton once per test file andRenderedAudiotapsaudioVisualizationEvents, 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 viahasRealEngine; hosts with a real soundcard will audibly play the test sounds. - new code gets coverage where practical; run
flutter testbefore 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.dartcompares 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 — withflutter 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 affecteddocs/user-guide/page for what the user sees, andCHANGELOG.mdunder the topmost## 0.0.x (unreleased)section for anything user-visible or packaging-related. - user-facing English strings go through
lib/l10n/app_en.arbonly — never hardcode UI text.
consistency
- follow the surrounding convention over introducing a new one: relative
imports inside
lib/,package:imports insidetest/, riverpod providers via@riverpod/@Riverpodcodegen, and generated files (*.g.dart,*.freezed.dart,*.drift.dart,lib/l10n/generated/) are never hand-edited.