Skip to content

Code conventions

The house rules that keep the codebase uniform across contributors and game packages. The linter and tool/verify.sh catch what they can; this page covers the conventions automation cannot.

Naming

  • Descriptive, not abbreviated. Names say what the value is: playerNames, previous, repository, metadata — never n, prev, repo, meta. Single-letter names are reserved for nothing; lambda and pattern parameters get real names too ((Player player) => …, (VoteEntry entry) => …), and loop counters are named for what they count (lineIndex, seatIndex), not i.
  • Common abbreviations are fine — id, url, csv, json, ui, db, plus the domain's own (tts, l10n) and framework-required names (ref, context). If an abbreviation wouldn't be said aloud in conversation, don't write it.
  • Case follows the language. Dart uses camelCase members and PascalCase types; shell variables are snake_case; Kotlin follows Kotlin convention.
  • Check terminology.md first. The glossary maps user-facing terms to code identifiers — a name that fights the glossary is wrong even when it's descriptive.

Ordering

  • Alphabetical when no stronger order exists — imports, barrel-file exports, pubspec dependencies, lint lists, enum members, and plain lists all sort A–Z.
  • Keep semantic order where the order itself means something: phase enums run in state-machine order, TurnTimerLevel/ SceneLength run low-to-high intensity, forbidden: clue lists run most-obvious-first, word pools group by grammar (alphabetical within a group), ARB keys group by feature. If the ordering rule isn't obvious from the code, a comment should say what it is.

Comments and documentation

  • Every declaration gets a doc comment (///) — public API and private classes, widgets, and State classes. Say what the thing is for and anything non-obvious about its lifecycle, not what the code already says. Generated files are exempt — they're regenerated anyway.
  • Use doc comments, not banner comments. A // ------ section header above a class duplicates what a doc comment does better; banners survive only for grouping free functions inside a long file.
  • Comment the why. Good comments explain constraints a reader can't see ("deferred past the build phase — the call mutates the controller"), not restate the next line.

Imports

  • lib/ uses relative imports (prefer_relative_imports), in each package's lib/ too.
  • test/ uses package: imports (package:librebash/…, package:game_mechanics/…).

Layout and styling

  • Never hardcode layout numbers. Spacing, radii, and recurring sizes come from the tokens in game_mechanics' app_spacing.dart (AppSpacing, AppRadius, AppSizes) — the same tokens serve the app and every game package. Missing a value? Add a token, don't inline a literal. See design-system.md.
  • Use the shared widgets rather than re-building the same shells — SessionScaffold, SessionTurnHeader, PassDeviceGate, AppIconButton, and friends are the conventions made concrete.

Strings

  • App text comes from lib/l10n/app_en.arb — never hardcode a user-facing string in lib/. The hardcoded_ui_strings test guards this.
  • Package text comes from <Name>Strings classes with English defaults; the app injects localized values through the scope. A new user-facing package string means a new field plus its ARB wiring.
  • Template placeholders are content contracts. Tokens like {n} or {p} inside authored strings and scene files are data formats — they aren't abbreviations and they don't get renamed.

Content

  • Every generated thing/phrase pairs with a CuratedPool. Any content a generator composes — titles, lines, headwords, survey questions, creature cards, review dishes and venues — also gets a hand-written list dealt through dealCuratedOrElse(random, deck, pool, generate): pool.share of deals serve an authored item, the rest run the generator. The flip is a mechanics roll on sessionRandomProvider; the authored draw goes through the shared DrawDeck under the pool's deckKey so it can't repeat until the list wraps.
  • A curated pool holds at least minCuratedPoolSize (50) entries. Per-package content tests assert the bar — a pool that ships under it fails its own test.
  • share defaults to defaultCuratedShare (0.10) and is tunable per pool. A pool whose authored voice is the whole point runs a hot share (Acquired Taste's scales: 0.85); keep the default unless the design calls for authored-dominant dealing.
  • Curated pool keys are authored constants like template-bank keys — stable, namespaced ('acquired_taste_curated_dishes'), and the deck reseeds itself when the list's size changes.
  • Deals that only select (a word off animals, a scene off monkeyScenes) are already curated — the pool is the authored list. The CuratedPool machinery is for pairing a generator with authored content, not for wrapping a pool that is one.

Generated files

  • *.g.dart, *.freezed.dart, lib/l10n/generated/** are never hand-edited. Change the source, rerun the generator, review the generated diff. See ARCHITECTURE.md.

Tests

  • Code changes ship with tests. Pure logic gets unit tests; widgets get testWidgets; every package owns its own test/ mirroring its lib/. If a seam is awkward to reach — a platform channel, a Speaker, a ScreenPinningService — it's already behind an interface for exactly that reason: fake it, don't skip the test.
  • Session tests stand on SessionTestRig (game_mechanics/lib/src/testing/): it builds the ProviderContainer with the session overrides (players/options/speaker/link/outcome), a FakeSessionLink, and the keep-alive listen — so a controller or bridge suite is a SessionTestRig(...) line plus assertions, not a rebuilt scaffold. RecordingSpeaker/RecordingSoundPlayer/ FakeSessionLink live in game_core/lib/src/testing/.
  • tool/verify.sh is the gate — format check, generators, analysis, and every test suite, in one script. Run it before pushing; .githooks/ automates the same.

Persisted names are contracts

Anything already written to storage, a wire format, or a shipped artifact is frozen: GameMetadata.id values, SharedPreferences keys, enum .name wire values, the screen-pinning channel name, package names, BUILD_TIMESTAMP, and string-template placeholders. The full list lives in terminology.md. Renaming one requires a migration story, not a refactor.

Remote wire schemas are versioned. A remote game's projection keys, intent kinds, and phase semantics are its wire contract — when you change any of them in a breaking way (renaming or removing a key, changing an intent's kind or payload shape, redefining a phase), bump that game's remoteCompatVersion. Additive fields, new content, and presentation tweaks stay compatible. Without the bump a peer on an older build joins silently and stalls — the version check is what turns schema drift into a legible "update to play" message.

Documentation

  • Docs describe current behavior. Update the dev guide (and any affected doc) in the same commit as the behavior change. ADRs are historical — leave them as the record of the decision, never edit them to look current, and never describe unbuilt designs as existing.
  • Update CHANGELOG.md for every user-visible or packaging change under the topmost ## 0.0.x (unreleased) heading.
  • Normal casing everywhere — no all-lowercase prose in docs, comments, or UI strings.

Commits

  • Conventional Commit format — feat(scope): …, fix: …, test: …, docs: …, refactor: …, build: …. Keep commits focused: a refactor and a behavior change are two commits.

Dependencies

  • F-Droid-clean only. Nothing that pulls Google Play Services or other proprietary blobs; a feature that truly needs one lands behind a dedicated build flavor — see ci-and-packaging.md. The material_ui/cupertino_ui dependency_overrides in the root pubspec are intentional (see the comment there) — don't remove them.