mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-11 13:47:38 +01:00
Some checks failed
Meals logged on the phone stopped reaching the PC, so the gate kept locking for slots that were already logged and the only way through was to spam "nothing". The PC's fetch was never broken: it was faithfully pulling a Firebase that did not contain the phone's meals. The phone had no working publish backstop on either transport: - `checkAndNotify` gated its sync on `due.isNotEmpty`. Logging every meal promptly means nothing is ever due, so the ~15-min periodic task never published. It was a notification job that happened to sync, not a sync timer. It now syncs on every tick it is asked to, then computes due slots from the merged result, so a meal pulled from another device suppresses a nag instead of triggering one. - `backgroundSyncPush` never called `initSyncDeviceId()`. A fresh isolate has its own static state, so `currentSyncDeviceId` fell back to the compile-time 'phone' constant and published to `devices/phone/` rather than the persisted uuid -- which then makes the next foreground push see `unchanged` and skip writing the uuid directory entirely. Separately, on the "Fetch from sync" path itself: `FirebaseSyncError` and `FirebaseAuthError` are siblings of `GitHubSyncError` under `RemoteSyncError`, not subclasses, so every per-request failure of the *primary* backend escaped `pull_shared_log`'s catch tuple and raised a traceback out of a helper documented to fail closed with a reason string. `ConfigError` needed the same treatment by a different route: it subclasses `Exception` directly, so it is outside `RemoteSyncError` too, and is now translated at `_client_for_run`. The PAT is no longer mandatory either. Firebase is primary, but `_read_token` ran before the client was ever built, so a Firebase-only machine could not sync at all -- the commits that fixed this touched only the Dart side. A stalled sync was invisible: the config gates returned silently and `openFirebase` swallowed every error. `dart:developer`'s `log()` does not reach logcat on a release build (verified on device), so logging alone would not have surfaced it. `SyncHealth` records each tick's outcome and the log screen shows a banner when publishing has actually stopped. Also make the real-Tk probes bypass the window manager. A tiling WM retiles a managed toplevel instead of honouring the requested geometry, so `winfo_height` reported 0/1px and the layout and focus-ring assertions failed on noise rather than on any regression -- 8 failures on the dev machine that only passed under Xvfb. `overrideredirect` makes the measurement deterministic with or without a WM; the probes still detect a genuine overflow at 1366x300. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RT8X32S9yDLW1Xf1PDq8z7
1102 lines
49 KiB
Markdown
1102 lines
49 KiB
Markdown
# CLAUDE.md — diet_guard
|
||
|
||
## What this does
|
||
|
||
A log-to-unlock gate: every ~30 minutes, `diet-guard-gate.timer` runs
|
||
`diet-guard-gate.service`, which checks whether a meal slot (08:00, 12:00,
|
||
16:00, 20:00) has elapsed without a logged meal. If so, it opens a fullscreen
|
||
Tk window that blocks the desktop until the user logs what they ate (with
|
||
autocomplete from a local food-name "bank", optionally seeded from Open Food
|
||
Facts via `requests`). It also tracks a daily calorie/macro budget, seeded
|
||
from biometrics at `init` time but freely editable afterward on either
|
||
device (PC gate or phone app), synced last-edit-wins like the food log (see
|
||
"Cross-device sync"). A second tab in the gate window ("History", built by
|
||
`_gatelock_calendar.py`) and a matching screen in the phone app show a
|
||
budget-adherence calendar, logging/adherence streaks, a year-to-date
|
||
tally, and weekly/monthly average intake, all derived from the same
|
||
day-status classification (`_daystatus.py` / `day_status_service.dart`) and
|
||
the averages built on it (`_averages.py` / `average_service.dart`).
|
||
|
||
See `docs/design.md` for the original feature spec (meal-slot timing logic,
|
||
the Tue/Wed/Thu "filled most of the day" catch-up rule). Note that the
|
||
multi-item **meal builder** described there was removed on 2026-07-26 as
|
||
unused, on both the gate and the phone; only its `components` *storage*
|
||
remains, so already-logged composite meals keep their per-item macros. Nothing
|
||
creates a new one. The phone's "repeat last meal" button and the
|
||
temptation-bundling reward prompt were removed in the same pass (the reward
|
||
was replaced by a post-log progress card).
|
||
|
||
## Scheduling
|
||
|
||
`diet-guard-gate.timer` — wall-clock `OnCalendar=*-*-* *:00/30:00`,
|
||
`Persistent=true`. Deliberately wall-clock rather than boot-relative: an
|
||
earlier boot-relative timer interacted badly with fullscreen games grabbing
|
||
keyboard/mouse input around the same point in their session, so this is
|
||
pinned to the clock instead of "N minutes after boot/login."
|
||
|
||
`diet-guard-gate.service` is `Type=oneshot`, fires every tick, and exits 0
|
||
immediately if no lock is due — cheap enough to run that often. It needs
|
||
`DISPLAY`/`XAUTHORITY` because it opens a Tk window; see the inline comments
|
||
in the unit file for why `XAUTHORITY` is pinned explicitly (a `Persistent=true`
|
||
catch-up run at session start can beat the display manager writing
|
||
`~/.Xauthority`) and why a real fix lives in Python (`wait_for_display()`)
|
||
rather than in the unit file.
|
||
|
||
## Cross-device sync
|
||
|
||
`diet-guard-sync.timer` fires `python -m diet_guard sync` every ~15 minutes
|
||
(headless, no `DISPLAY` needed — separate from the gate timer on purpose, see
|
||
the unit file's comment for why). It pulls every other device's pushed log
|
||
from the private `kuhyx/syncs` GitHub repo (under `diet-guard-sync/`, used as dumb file
|
||
storage via the REST Contents API, not a git clone), merges with the local
|
||
log (`_sync_merge.merge_logs`: union by `id`, tombstone wins, legacy
|
||
`(time, desc)` dedup for pre-`id` entries), **re-signs every persisted entry**
|
||
regardless of origin, rebuilds the food bank, then pushes this device's own
|
||
merged log back up.
|
||
|
||
**The phone's periodic tick must sync unconditionally.** `checkAndNotify`
|
||
(`due_slot_check.dart`) once gated its sync on `due.isNotEmpty` — "only pull
|
||
when a slot looks overdue". That is a trap: logging every meal promptly on the
|
||
phone means nothing is ever due, so the phone never published, and the PC's
|
||
gate kept locking for slots that *were* already logged (the only way through
|
||
being to spam "nothing"). It publishes first, then computes due slots from the
|
||
merged result, so a meal pulled from another device suppresses a nag instead of
|
||
triggering one. Do not re-gate the sync on the notification's condition.
|
||
|
||
**Anything running in a WorkManager background isolate must call
|
||
`initSyncDeviceId()` itself.** A fresh isolate has its own static state, so
|
||
without it `currentSyncDeviceId` silently falls back to the compile-time
|
||
`'phone'`/`'desktop'` role constant and the tick publishes to
|
||
`devices/phone/` instead of the persisted uuid. Because `sync_state` holds a
|
||
single `pushedRev` with no notion of which identity wrote it, the next
|
||
*foreground* push then sees `unchanged` and skips writing the uuid directory
|
||
entirely. `background_sync_service.dart` does this alongside its other
|
||
singleton re-inits; `background_sync_service_test.dart` guards it.
|
||
|
||
**Catch `RemoteSyncError`, never `GitHubSyncError`, on any sync path.**
|
||
`FirebaseSyncError`/`FirebaseAuthError` are *siblings* of `GitHubSyncError`
|
||
under `RemoteSyncError`, not subclasses. Catching the GitHub type alone lets
|
||
every per-request failure of the *primary* backend escape — which made
|
||
`pull_shared_log` (the gate's "Fetch from sync" button, documented to fail
|
||
closed with a reason string) raise a traceback instead.
|
||
`ConfigError` is the one to watch: it subclasses `Exception` **directly**, so
|
||
`RemoteSyncError` does not cover it either. Anything that builds a Firebase
|
||
client must translate it (`_client_for_run` raises `SyncError`), or it escapes
|
||
every caller's catch tuple by a different route.
|
||
|
||
Re-signing on every merge (not just phone-origin entries) is the
|
||
non-negotiable step: `_entry_is_valid()` drops any unsigned entry once a
|
||
machine has the shared HMAC key, and the phone never holds that key, so
|
||
skipping the re-sign would silently lose every phone-logged meal on the very
|
||
next read.
|
||
|
||
Needs **one** backend configured, not both. Either `~/.config/crdt-sync/`
|
||
(Firebase, now the primary) or a fine-grained GitHub PAT scoped to `syncs`'s
|
||
contents (read/write) at `~/.config/diet_guard/sync_token`, mode 600 —
|
||
a one-time manual setup `install.sh` does **not** automate. A
|
||
Firebase-configured machine with no PAT syncs against Firebase alone
|
||
(`_client_for_run`); only when *neither* exists is the tick a harmless no-op
|
||
that logs `sync not configured`. The PAT used to be required unconditionally,
|
||
which left a Firebase-only PC unable to sync at all.
|
||
|
||
**Both halves of the food bank sync.** The derived bank
|
||
(`food_bank.json`) is still rebuilt locally from the merged log
|
||
(`_foodbank.rebuild_food_bank`), but it is then merged and pushed too, so the
|
||
two devices agree *immediately* instead of only after each has replayed the
|
||
log. Its merge clock is the record's own **`count`**, not a wall time: LWW
|
||
therefore means *max-count-wins*, which is the correct merge for a derived
|
||
counter (the device that has seen more of the log has the higher count) and
|
||
is idempotent, because the count only moves when the log does. That is what
|
||
sidesteps needing a real counter CRDT.
|
||
|
||
The **hand-curated** bank also syncs
|
||
(`food_bank_manual.json`, `_foodbank_manual.py` / the manual half of
|
||
`foodbank_service.dart`). Those are foods the user added without ever eating
|
||
them, so they are not derivable from any log; before they synced they were
|
||
the last piece of device-local state in the app. One CRDT record per
|
||
normalized name, last-writer-wins by an `editedAt` stamp, union across
|
||
devices. `lookup_food`/`search_foods` merge them under the log-derived
|
||
records, so a food curated on the phone autocompletes in the PC gate.
|
||
|
||
## Production dependency installation — read this before adding any dependency
|
||
|
||
`diet-guard-gate.service` runs `/usr/bin/python` directly — **not** a venv.
|
||
Any new non-stdlib dependency (this package itself, `gatelock`, `requests`,
|
||
anything added later) must be installed into system Python's *user*
|
||
site-packages, the same place `python-kasa` already lives:
|
||
|
||
```bash
|
||
/usr/bin/python3 -m pip install --user --break-system-packages -e .
|
||
```
|
||
|
||
`install.sh` already does this. **If you add a dependency and only install it
|
||
into a dev venv, the production service will silently fail with
|
||
`ModuleNotFoundError` on its next tick** — this exact gap caused a 3-day
|
||
diet_guard production outage (2026-06-19 to 2026-06-22) when `gatelock` was
|
||
added but only `pip install`-ed into `.venv`. Always verify against
|
||
`/usr/bin/python3 -c "import <new_dep>"`, not just the dev venv, before
|
||
considering a dependency change done.
|
||
|
||
**Always run `install.sh` (or `pip install -e`) from a durable clone, not a
|
||
scratch directory.** `install.sh` does `pip install -e "$REPO_DIR"` —
|
||
editable, so a later `git pull` in that same clone updates the running
|
||
production code with no reinstall needed. The clone must live somewhere
|
||
permanent (this repo's convention: `~/diet-guard`, mirroring `~/screen-locker`
|
||
for the screen-locker package) — if you `pip install -e` from `/tmp/...` or
|
||
run `pip install "diet_guard @ git+https://..."` as a one-off, you get a
|
||
non-editable snapshot frozen at that commit, and the next `git push` here
|
||
silently does **not** reach the running service.
|
||
|
||
## Operational gotchas
|
||
|
||
- **The budget is a plain, freely-editable synced file, by design.**
|
||
`~/.local/share/diet_guard/.budget` (`diet_guard/_budget.py`) has no seal
|
||
or signing — this replaces a previous `chattr +i` anti-impulse-editing
|
||
mechanism, removed deliberately so the same value can be edited from
|
||
either device. Edit it from the gate's History tab, the phone app's
|
||
Calendar screen, or `settings_screen.dart`; every write stamps an edit
|
||
timestamp so `diet-guard-sync.timer`'s `budget.json` sync (mirroring the
|
||
food-log sync) resolves concurrent edits last-writer-wins. Do not
|
||
reintroduce the seal as a "fix" — the removal is the feature.
|
||
- **Past days are judged against the budget that applied then.**
|
||
`~/.local/share/diet_guard/.budget_history` (`diet_guard/_budget_history.py`,
|
||
mirrored by `app/lib/services/budget_schedule.dart`) is a forward-only list
|
||
of `(effective_from, kcal)` entries; `status_map`/`statusMap` resolve each
|
||
day through it, so lowering the budget never retroactively breaks the
|
||
adherence streak or the YTD tally. It syncs as extra `hist:<YYYY-MM-DD>`
|
||
*fields* on the existing `budget` CRDT record rather than as a second
|
||
document: `merge_record` is per-field LWW over the **union** of field names
|
||
and both devices push the *merged* log, so a device that predates the
|
||
feature relays those fields untouched. `write_budget` /
|
||
`saveDailyKcalGoal` must seed the pre-write value to `1970-01-01` **before**
|
||
recording the new one for today — reversing that makes every past day adopt
|
||
the new budget, which is the exact bug this exists to prevent.
|
||
- **Biometrics are used once and discarded.** `init` asks for biometrics to
|
||
compute the daily budget, then the only persisted output is the computed
|
||
budget number — never the biometrics themselves.
|
||
- **PC and phone share one source of truth for every piece of data.** The
|
||
food log, the budget, the budget history, body weight (`w`, its own
|
||
`weight` CRDT field), and the curated food bank all round-trip through
|
||
`kuhyx/syncs`. The derived food bank is a local cache of the synced log,
|
||
not separate state. There is deliberately nothing left that only one
|
||
device knows — if you add a new stored field, sync it or explain in a
|
||
comment why it physically cannot be shared.
|
||
- **State lives entirely under `~/.local/share/diet_guard/`** — no
|
||
cross-repo file coupling (unlike wake_alarm, which reads
|
||
`~/screen-locker/screen_locker/workout_log.json`). Safe to reason about in
|
||
isolation, with one exception: `diet-guard-sync.timer` reads/writes the
|
||
private `kuhyx/syncs` GitHub repo (see "Cross-device sync" above)
|
||
and `~/.config/diet_guard/sync_token`.
|
||
|
||
## The companion app's desktop target is a web build
|
||
|
||
`app/` builds for two platforms, and only two: **Android** (the phone) and
|
||
**web** (the Linux desktop). There is no `app/linux/` — Flutter's GTK embedder
|
||
manages only ~20fps at 3840x2160 (measured on a stock `flutter create` app, so
|
||
it is the toolkit, not this code), where the same Dart in Chrome sustains
|
||
~144fps; see `~/todo/docs/desktop-performance-findings.md`. On the desktop the
|
||
app is the web build served by `bin/diet_guard_desktop.dart` and shown in a
|
||
Chrome `--app` window.
|
||
|
||
Consequences worth knowing before touching `app/`:
|
||
|
||
- **`dart:io` does not fail a web *compile*** — it becomes a stub that throws
|
||
at runtime, so the symptom is a blank white window, not a build error. Every
|
||
platform-specific edge goes behind a conditional export
|
||
(`document_store*.dart`, `blob_store*.dart`, `background_tasks*.dart`,
|
||
`notification_backend*.dart`, `token_vault*.dart`,
|
||
`github_client_factory*.dart`, `attached_image*.dart`). Branch on `kIsWeb`
|
||
*before* any `Platform.is…`, which itself throws on web.
|
||
- **The port and the Chrome `--user-data-dir` are fixed on purpose**
|
||
(`lib/services/desktop_wrapper.dart`). IndexedDB is keyed by origin and
|
||
lives in that profile: changing either hides the entire local food log.
|
||
- **The wrapper holds the GitHub token**, not the browser, and proxies
|
||
`api.github.com` plus the CORS-less device flow (`lib/desktop/github_proxy.dart`).
|
||
- **Every device pushes under a persisted per-install uuid**, not the old
|
||
fixed `pc`/`phone`/`desktop` role constants. A role constant collides the
|
||
moment a second machine takes the same role, and a reinstall silently
|
||
inherits the previous install's CRDT identity. The id lives in
|
||
`~/.local/share/diet_guard/.device_id` on the PC (`diet_guard/_device.py`)
|
||
and under SharedPreferences `crdt.nodeId` in the app
|
||
(`lib/services/sync_device_id.dart`), the same key `todo` and
|
||
`home_inventory` use.
|
||
- The old role constant survives as the **legacy id**
|
||
(`SYNC_LEGACY_DEVICE_ID` / `legacySyncDeviceId`), passed to `sync_log` /
|
||
`syncLog` so `devices/<legacy>/` is skipped as *this device's own*. Drop
|
||
it to `None`/null only after that path has been deleted — until then,
|
||
removing it makes every tick re-download and re-merge this device's own
|
||
pre-migration log as though a peer wrote it.
|
||
- The app resolves its id **asynchronously at startup**
|
||
(`initSyncDeviceId()` in `main.dart`, before `LogStorageService.init()`).
|
||
HLC stamping is synchronous, so `currentSyncDeviceId` falls back to the
|
||
compile-time role constant until that completes: a caller that stamps too
|
||
early gets a valid-but-pre-migration id rather than a crash. Don't move
|
||
that call later.
|
||
- **Tests must redirect `_device.SYNC_DEVICE_ID_FILE`** (conftest already
|
||
does). Without it a test mints a uuid into the real state dir and this
|
||
machine's live sync identity is decided by whichever test ran first.
|
||
- **Desktop reminders only exist while the window is open.** A browser has no
|
||
WorkManager equivalent; the PC's real backstop is `diet-guard-gate.timer`,
|
||
which locks the screen. Don't "fix" this with a background service without
|
||
deciding you want a new daemon.
|
||
- `flutter create --platforms linux` would silently restore the slow path.
|
||
Don't run it in `app/`.
|
||
|
||
## Commands
|
||
|
||
- Run tests: `python -m pytest diet_guard/tests/ --cov=diet_guard --cov-branch --cov-fail-under=100`
|
||
- Lint: `pre-commit run --all-files`
|
||
- Test the lock manually (safe, closeable): `python -m diet_guard gate --demo`
|
||
- Run one sync tick manually: `python -m diet_guard sync`
|
||
- Weekly/monthly averages: `python -m diet_guard averages`
|
||
- Install for production: `bash install.sh`
|
||
- App tests: `cd app && flutter test`
|
||
- Run the desktop app from the repo: `cd app && bash run.sh`
|
||
- Install the desktop app (Arch package + .desktop entry):
|
||
`cd app && bash install_arch.sh`
|
||
- Build the phone app: `cd app && flutter build apk --release`
|
||
|
||
## Averages (`_averages.py` / `average_service.dart`)
|
||
|
||
Three rules are load-bearing; changing any of them silently changes what
|
||
"under budget" means, so re-read this before touching them:
|
||
|
||
- **Denominator is logged days, not elapsed days.** An unlogged day is not a
|
||
zero-kcal day. Averaging gaps in as zeros makes a badly-tracked week look
|
||
like the best week of the year, which is the exact failure the gate exists
|
||
to prevent.
|
||
- **The yardstick is the mean of the per-day budgets over those same logged
|
||
days**, resolved through the `BudgetSchedule` -- never `daily_budget()`.
|
||
Using today's number would retroactively reclassify past weeks on every
|
||
budget edit, the same bug `_budget_history.py` was written to fix.
|
||
- **Today is excluded.** Every period ends at `last_complete_day()`, i.e.
|
||
yesterday. A period ending "now" mixes finished days with one that is three
|
||
hours old, and a half-logged today drags the mean far enough to flip the
|
||
band. The visible cost is that "this week" is empty on a Monday; that is
|
||
correct, and it reports `no logged days yet` rather than a fake average.
|
||
|
||
Band boundaries reuse `_daystatus.OVER_BUDGET_YELLOW_CEILING` /
|
||
`kOverBudgetYellowCeiling` **by import, not by copy** -- two literals would let
|
||
the averages line and the calendar disagree about what "over" means.
|
||
|
||
The MCP `get_averages` tool returns `avg_kcal` and the band but deliberately
|
||
drops `avg_budget`, keeping the module's budget-secrecy invariant. The CLI,
|
||
the gate, and the phone all show both numbers, since the budget is hidden only
|
||
from the network, not from the user.
|
||
|
||
## Do NOT
|
||
|
||
- Don't relax the meal-slot logic without re-reading `docs/design.md` — the
|
||
Tue/Wed/Thu catch-up rule is deliberate, not accidental complexity. The
|
||
off-hours **clamp** in `slot_for_log`/`slotForLog` is likewise deliberate
|
||
(a meal before 08:00 counts toward 08:00, after 22:00 toward 20:00) and
|
||
must stay byte-identical across Python and Dart; do NOT "fix" it by
|
||
widening `elapsed_slots`, which would make every slot fall due at 23:00.
|
||
- Don't re-add the meal builder, the "repeat last meal" button, the reward
|
||
prompt, or **meal photos** — all four were removed deliberately as unused.
|
||
The photo removal took `image_picker`, the blob stores, the wrapper's
|
||
`/blobs/` route and the `CAMERA` permission with it; re-adding photos means
|
||
re-adding all of that *plus* syncing image blobs, since a device-local
|
||
attachment would violate the shared-source-of-truth rule above. Do not strip the
|
||
`components` field either: nothing writes it any more, but historical
|
||
composite entries and the food bank still read it, and it is part of the
|
||
sync wire format.
|
||
- Don't add a dependency without doing the production install-path check
|
||
above.
|
||
- Don't reintroduce a seal/lock/`chattr +i` on the budget file as a "fix" —
|
||
it was deliberately removed so the budget is freely editable and synced
|
||
between devices; see "Operational gotchas" above.
|
||
- Don't re-add a Linux embedder target to `app/`, and don't change the
|
||
wrapper's fixed port or Chrome profile path — see "The companion app's
|
||
desktop target is a web build" above.
|
||
|
||
## Flutter/Dart AI rules
|
||
|
||
_Appended from [flutter/flutter docs/rules/rules.md](https://github.com/flutter/flutter/blob/main/docs/rules/rules.md) via `/home/kuhy/.claude/CLAUDE.md` Flutter AI tooling setup. Re-fetch periodically to stay current._
|
||
|
||
## AI rules for Flutter
|
||
|
||
You are an expert in Flutter and Dart development. Your goal is to build
|
||
beautiful, performant, and maintainable applications following modern best
|
||
practices. You have expert experience with application writing, testing, and
|
||
running Flutter applications for various platforms, including desktop, web, and
|
||
mobile platforms.
|
||
|
||
### Interaction Guidelines
|
||
* **User Persona:** Assume the user is familiar with programming concepts but
|
||
may be new to Dart.
|
||
* **Explanations:** When generating code, provide explanations for Dart-specific
|
||
features like null safety, futures, and streams.
|
||
* **Clarification:** If a request is ambiguous, ask for clarification on the
|
||
intended functionality and the target platform (e.g., command-line, web,
|
||
server).
|
||
* **Dependencies:** When suggesting new dependencies from `pub.dev`, explain
|
||
their benefits.
|
||
* **Formatting:** Use the `dart_format` tool to ensure consistent code
|
||
formatting.
|
||
* **Fixes:** Use the `dart_fix` tool to automatically fix many common errors,
|
||
and to help code conform to configured analysis options.
|
||
* **Linting:** Use the Dart linter with a recommended set of rules to catch
|
||
common issues. Use the `analyze_files` tool to run the linter.
|
||
|
||
### Project Structure
|
||
* **Standard Structure:** Assumes a standard Flutter project structure with
|
||
`lib/main.dart` as the primary application entry point.
|
||
|
||
### Flutter style guide
|
||
* **SOLID Principles:** Apply SOLID principles throughout the codebase.
|
||
* **Concise and Declarative:** Write concise, modern, technical Dart code.
|
||
Prefer functional and declarative patterns.
|
||
* **Composition over Inheritance:** Favor composition for building complex
|
||
widgets and logic.
|
||
* **Immutability:** Prefer immutable data structures. Widgets (especially
|
||
`StatelessWidget`) should be immutable.
|
||
* **State Management:** Separate ephemeral state and app state. Use a state
|
||
management solution for app state to handle the separation of concerns.
|
||
* **Widgets are for UI:** Everything in Flutter's UI is a widget. Compose
|
||
complex UIs from smaller, reusable widgets.
|
||
* **Navigation:** Use a modern routing package like `auto_route` or `go_router`.
|
||
For more guidelines around navigation, see the section on [routing](#routing).
|
||
|
||
### Package Management
|
||
* **Pub Tool:** To manage packages, use the `pub` tool, if available.
|
||
* **External Packages:** If a new feature requires an external package, use the
|
||
`pub_dev_search` tool, if it is available. Otherwise, identify the most
|
||
suitable and stable package from pub.dev.
|
||
* **Adding Dependencies:** To add a regular dependency, use the `pub` tool, if
|
||
it is available. Otherwise, run `flutter pub add <package_name>`.
|
||
* **Adding Dev Dependencies:** To add a development dependency, use the `pub`
|
||
tool, if it is available, with `dev:<package name>`. Otherwise, run `flutter
|
||
pub add dev:<package_name>`.
|
||
* **Dependency Overrides:** To add a dependency override, use the `pub` tool, if
|
||
it is available, with `override:<package name>:1.0.0`. Otherwise, run `flutter
|
||
pub add override:<package_name>:1.0.0`.
|
||
* **Removing Dependencies:** To remove a dependency, use the `pub` tool, if it
|
||
is available. Otherwise, run `dart pub remove <package_name>`.
|
||
|
||
### Code Quality
|
||
* **Code structure:** Adhere to maintainable code structure and separation of
|
||
concerns (e.g., UI logic separate from business logic).
|
||
* **Naming conventions:** Avoid abbreviations and use meaningful, consistent,
|
||
descriptive names for variables, functions, and classes.
|
||
* **Conciseness:** Write code that is as short as it can be while remaining
|
||
clear.
|
||
* **Simplicity:** Write straightforward code. Code that is clever or
|
||
obscure is difficult to maintain.
|
||
* **Error Handling:** Anticipate and handle potential errors. Don't let your
|
||
code fail silently.
|
||
* **Styling:**
|
||
* Line length: Lines should be 80 characters or fewer.
|
||
* Use `PascalCase` for classes, `camelCase` for
|
||
members/variables/functions/enums, and `snake_case` for files.
|
||
* **Functions:**
|
||
* Keep functions short and with a single purpose.
|
||
Strive for less than 20 lines.
|
||
* **Testing:** Write code with testing in mind. Use the `file`, `process`, and
|
||
`platform` packages, if appropriate, so you can inject in-memory and fake
|
||
versions of the objects.
|
||
* **Logging:** Use the `logging` package instead of `print`.
|
||
|
||
### Dart Best Practices
|
||
* **Effective Dart:** Follow the official Effective Dart guidelines
|
||
(https://dart.dev/effective-dart)
|
||
* **Class Organization:** Define related classes within the same library file.
|
||
For large libraries, export smaller, private libraries from a single top-level
|
||
library.
|
||
* **Library Organization:** Group related libraries in the same folder.
|
||
* **API Documentation:** Add documentation comments to all public APIs,
|
||
including classes, constructors, methods, and top-level functions.
|
||
* **Comments:** Write clear comments for complex or non-obvious code. Avoid
|
||
over-commenting.
|
||
* **Trailing Comments:** Don't add trailing comments.
|
||
* **Async/Await:** Ensure proper use of `async`/`await` for asynchronous
|
||
operations with robust error handling.
|
||
* Use `Future`s, `async`, and `await` for asynchronous operations.
|
||
* Use `Stream`s for sequences of asynchronous events.
|
||
* **Null Safety:** Write code that is soundly null-safe. Leverage Dart's null
|
||
safety features. Avoid `!` unless the value is guaranteed to be non-null.
|
||
* **Pattern Matching:** Use pattern matching features where they simplify the
|
||
code.
|
||
* **Records:** Use records to return multiple types in situations where defining
|
||
an entire class is cumbersome.
|
||
* **Switch Statements:** Prefer using exhaustive `switch` statements or
|
||
expressions, which don't require `break` statements.
|
||
* **Exception Handling:** Use `try-catch` blocks for handling exceptions, and
|
||
use exceptions appropriate for the type of exception. Use custom exceptions
|
||
for situations specific to your code.
|
||
* **Arrow Functions:** Use arrow syntax for simple one-line functions.
|
||
|
||
### Flutter Best Practices
|
||
* **Immutability:** Widgets (especially `StatelessWidget`) are immutable; when
|
||
the UI needs to change, Flutter rebuilds the widget tree.
|
||
* **Composition:** Prefer composing smaller widgets over extending existing
|
||
ones. Use this to avoid deep widget nesting.
|
||
* **Private Widgets:** Use small, private `Widget` classes instead of private
|
||
helper methods that return a `Widget`.
|
||
* **Build Methods:** Break down large `build()` methods into smaller, reusable
|
||
private Widget classes.
|
||
* **List Performance:** Use `ListView.builder` or `SliverList` for long lists to
|
||
create lazy-loaded lists for performance.
|
||
* **Isolates:** Use `compute()` to run expensive calculations in a separate
|
||
isolate to avoid blocking the UI thread, such as JSON parsing.
|
||
* **Const Constructors:** Use `const` constructors for widgets and in `build()`
|
||
methods whenever possible to reduce rebuilds.
|
||
* **Build Method Performance:** Avoid performing expensive operations, like
|
||
network calls or complex computations, directly within `build()` methods.
|
||
|
||
### API Design Principles
|
||
When building reusable APIs, such as a library, follow these principles.
|
||
|
||
* **Consider the User:** Design APIs from the perspective of the person who will
|
||
be using them. The API should be intuitive and easy to use correctly.
|
||
* **Documentation is Essential:** Good documentation is a part of good API
|
||
design. It should be clear, concise, and provide examples.
|
||
|
||
### Application Architecture
|
||
* **Separation of Concerns:** Aim for separation of concerns similar to MVC/MVVM, with defined Model,
|
||
View, and ViewModel/Controller roles.
|
||
* **Logical Layers:** Organize the project into logical layers:
|
||
* Presentation (widgets, screens)
|
||
* Domain (business logic classes)
|
||
* Data (model classes, API clients)
|
||
* Core (shared classes, utilities, and extension types)
|
||
* **Feature-based Organization:** For larger projects, organize code by feature,
|
||
where each feature has its own presentation, domain, and data subfolders. This
|
||
improves navigability and scalability.
|
||
|
||
### Lint Rules
|
||
|
||
Include the package in the `analysis_options.yaml` file. Use the following
|
||
`analysis_options.yaml` file as a starting point:
|
||
|
||
```yaml
|
||
include: package:flutter_lints/flutter.yaml
|
||
|
||
linter:
|
||
rules:
|
||
# Add additional lint rules here:
|
||
# avoid_print: false
|
||
# prefer_single_quotes: true
|
||
```
|
||
|
||
#### State Management
|
||
* **Built-in Solutions:** Prefer Flutter's built-in state management solutions.
|
||
Do not use a third-party package unless explicitly requested.
|
||
* **Streams:** Use `Streams` and `StreamBuilder` for handling a sequence of
|
||
asynchronous events.
|
||
* **Futures:** Use `Futures` and `FutureBuilder` for handling a single
|
||
asynchronous operation that will complete in the future.
|
||
* **ValueNotifier:** Use `ValueNotifier` with `ValueListenableBuilder` for
|
||
simple, local state that involves a single value.
|
||
|
||
```dart
|
||
// Define a ValueNotifier to hold the state.
|
||
final ValueNotifier<int> _counter = ValueNotifier<int>(0);
|
||
|
||
// Use ValueListenableBuilder to listen and rebuild.
|
||
ValueListenableBuilder<int>(
|
||
valueListenable: _counter,
|
||
builder: (context, value, child) {
|
||
return Text('Count: $value');
|
||
},
|
||
);
|
||
```
|
||
|
||
* **ChangeNotifier:** For state that is more complex or shared across multiple
|
||
widgets, use `ChangeNotifier`.
|
||
* **ListenableBuilder:** Use `ListenableBuilder` to listen to changes from a
|
||
`ChangeNotifier` or other `Listenable`.
|
||
* **MVVM:** When a more robust solution is needed, structure the app using the
|
||
Model-View-ViewModel (MVVM) pattern.
|
||
* **Dependency Injection:** Use simple manual constructor dependency injection
|
||
to make a class's dependencies explicit in its API, and to manage dependencies
|
||
between different layers of the application.
|
||
* **Provider:** If a dependency injection solution beyond manual constructor
|
||
injection is explicitly requested, `provider` can be used to make services,
|
||
repositories, or complex state objects available to the UI layer without tight
|
||
coupling (note: this document generally defaults against third-party packages
|
||
for state management unless explicitly requested).
|
||
|
||
#### Data Flow
|
||
* **Data Structures:** Define data structures (classes) to represent the data
|
||
used in the application.
|
||
* **Data Abstraction:** Abstract data sources (e.g., API calls, database
|
||
operations) using Repositories/Services to promote testability.
|
||
|
||
#### Routing
|
||
* **GoRouter:** Use the `go_router` package for declarative navigation, deep
|
||
linking, and web support.
|
||
* **GoRouter Setup:** To use `go_router`, first add it to your `pubspec.yaml`
|
||
using the `pub` tool's `add` command.
|
||
|
||
```dart
|
||
// 1. Add the dependency
|
||
// flutter pub add go_router
|
||
|
||
// 2. Configure the router
|
||
final GoRouter _router = GoRouter(
|
||
routes: <RouteBase>[
|
||
GoRoute(
|
||
path: '/',
|
||
builder: (context, state) => const HomeScreen(),
|
||
routes: <RouteBase>[
|
||
GoRoute(
|
||
path: 'details/:id', // Route with a path parameter
|
||
builder: (context, state) {
|
||
final String id = state.pathParameters['id']!;
|
||
return DetailScreen(id: id);
|
||
},
|
||
),
|
||
],
|
||
),
|
||
],
|
||
);
|
||
|
||
// 3. Use it in your MaterialApp
|
||
MaterialApp.router(
|
||
routerConfig: _router,
|
||
);
|
||
```
|
||
* **Authentication Redirects:** Configure `go_router`'s `redirect` property to
|
||
handle authentication flows, ensuring users are redirected to the login screen
|
||
when unauthorized, and back to their intended destination after successful
|
||
login.
|
||
|
||
* **Navigator:** Use the built-in `Navigator` for short-lived screens that do
|
||
not need to be deep-linkable, such as dialogs or temporary views.
|
||
|
||
```dart
|
||
// Push a new screen onto the stack
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(builder: (context) => const DetailsScreen()),
|
||
);
|
||
|
||
// Pop the current screen to go back
|
||
Navigator.pop(context);
|
||
```
|
||
|
||
#### Data Handling & Serialization
|
||
* **JSON Serialization:** Use `json_serializable` and `json_annotation` for
|
||
parsing and encoding JSON data.
|
||
* **Field Renaming:** When encoding data, use `fieldRename: FieldRename.snake`
|
||
to convert Dart's camelCase fields to snake_case JSON keys.
|
||
|
||
```dart
|
||
// In your model file
|
||
import 'package:json_annotation/json_annotation.dart';
|
||
|
||
part 'user.g.dart';
|
||
|
||
@JsonSerializable(fieldRename: FieldRename.snake)
|
||
class User {
|
||
final String firstName;
|
||
final String lastName;
|
||
|
||
User({required this.firstName, required this.lastName});
|
||
|
||
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
|
||
Map<String, dynamic> toJson() => _$UserToJson(this);
|
||
}
|
||
```
|
||
|
||
|
||
#### Logging
|
||
* **Structured Logging:** Use the `log` function from `dart:developer` for
|
||
structured logging that integrates with Dart DevTools.
|
||
|
||
```dart
|
||
import 'dart:developer' as developer;
|
||
|
||
// For simple messages
|
||
developer.log('User logged in successfully.');
|
||
|
||
// For structured error logging
|
||
try {
|
||
// ... code that might fail
|
||
} catch (e, s) {
|
||
developer.log(
|
||
'Failed to fetch data',
|
||
name: 'myapp.network',
|
||
level: 1000, // SEVERE
|
||
error: e,
|
||
stackTrace: s,
|
||
);
|
||
}
|
||
```
|
||
|
||
### Code Generation
|
||
* **Build Runner:** If the project uses code generation, ensure that
|
||
`build_runner` is listed as a dev dependency in `pubspec.yaml`.
|
||
* **Code Generation Tasks:** Use `build_runner` for all code generation tasks,
|
||
such as for `json_serializable`.
|
||
* **Running Build Runner:** After modifying files that require code generation,
|
||
run the build command:
|
||
|
||
```shell
|
||
dart run build_runner build --delete-conflicting-outputs
|
||
```
|
||
|
||
### Testing
|
||
* **Running Tests:** To run tests, use the `run_tests` tool if it is available,
|
||
otherwise use `flutter test`.
|
||
* **Unit Tests:** Use `package:test` for unit tests.
|
||
* **Widget Tests:** Use `package:flutter_test` for widget tests.
|
||
* **Integration Tests:** Use `package:integration_test` for integration tests.
|
||
* **Assertions:** Prefer using `package:checks` for more expressive and readable
|
||
assertions over the default `matchers`.
|
||
|
||
#### Testing Best practices
|
||
* **Convention:** Follow the Arrange-Act-Assert (or Given-When-Then) pattern.
|
||
* **Unit Tests:** Write unit tests for domain logic, data layer, and state
|
||
management.
|
||
* **Widget Tests:** Write widget tests for UI components.
|
||
* **Integration Tests:** For broader application validation, use integration
|
||
tests to verify end-to-end user flows.
|
||
* **integration_test package:** Use the `integration_test` package from the
|
||
Flutter SDK for integration tests. Add it as a `dev_dependency` in
|
||
`pubspec.yaml` by specifying `sdk: flutter`.
|
||
* **Mocks:** Prefer fakes or stubs over mocks. If mocks are absolutely
|
||
necessary, use `mockito` or `mocktail` to create mocks for dependencies. While
|
||
code generation is common for state management (e.g., with `freezed`), try to
|
||
avoid it for mocks.
|
||
* **Coverage:** Aim for high test coverage.
|
||
|
||
### Visual Design & Theming
|
||
* **UI Design:** Build beautiful and intuitive user interfaces that follow
|
||
modern design guidelines.
|
||
* **Responsiveness:** Ensure the app is mobile responsive and adapts to
|
||
different screen sizes, working perfectly on mobile and web.
|
||
* **Navigation:** If there are multiple pages for the user to interact with,
|
||
provide an intuitive and easy navigation bar or controls.
|
||
* **Typography:** Stress and emphasize font sizes to ease understanding, e.g.,
|
||
hero text, section headlines, list headlines, keywords in paragraphs.
|
||
* **Background:** Keep backgrounds flat and simple — no noise texture or
|
||
gradients. Follow the `unified-design-system` skill (safe-design-rules
|
||
rule 17: simple-on-complex); let content and typography carry visual
|
||
interest instead of background texture.
|
||
* **Shadows:** Follow the `unified-design-system` skill's shadow policy: no
|
||
shadows at all in dark UI (differentiate elevation with lighter surface
|
||
fills, not shadows); in light UI, shadows are for floating/overlay
|
||
elements only (menus, dialogs), never for inline cards, and never mixed
|
||
with a border on the same surface.
|
||
* **Icons:** Incorporate icons to enhance the user’s understanding and the
|
||
logical navigation of the app.
|
||
* **Interactive Elements:** Buttons/controls signal state via color and fill
|
||
changes, not a "glow" shadow — one depth technique per surface, per
|
||
`unified-design-system`.
|
||
|
||
#### Theming
|
||
* **Centralized Theme:** Define a centralized `ThemeData` object to ensure a
|
||
consistent application-wide style.
|
||
* **Light and Dark Themes:** Implement support for both light and dark themes,
|
||
ideal for a user-facing theme toggle (`ThemeMode.light`, `ThemeMode.dark`,
|
||
`ThemeMode.system`).
|
||
* **Color Scheme Generation:** Do NOT use `ColorScheme.fromSeed` for this
|
||
app's palette — it can't reproduce hand-picked values exactly. Build
|
||
`ThemeData` from explicit `ColorScheme.dark(...)`/`.light(...)` using the
|
||
frozen, identical-everywhere tokens in the `unified-design-system` skill
|
||
(`~/.claude/skills/unified-design-system/references/tokens.md`).
|
||
|
||
```dart
|
||
final ThemeData lightTheme = ThemeData(
|
||
colorScheme: ColorScheme.fromSeed(
|
||
seedColor: Colors.deepPurple,
|
||
brightness: Brightness.light,
|
||
),
|
||
// ... other theme properties
|
||
);
|
||
```
|
||
* **Color Palette:** Include a wide range of color concentrations and hues in
|
||
the palette to create a vibrant and energetic look and feel.
|
||
* **Component Themes:** Use specific theme properties (e.g., `appBarTheme`,
|
||
`elevatedButtonTheme`) to customize the appearance of individual Material
|
||
components.
|
||
* **Custom Fonts:** For custom fonts, use the `google_fonts` package. Define a
|
||
`TextTheme` to apply fonts consistently.
|
||
|
||
```dart
|
||
// 1. Add the dependency
|
||
// flutter pub add google_fonts
|
||
|
||
// 2. Define a TextTheme with a custom font
|
||
final TextTheme appTextTheme = TextTheme(
|
||
displayLarge: GoogleFonts.oswald(fontSize: 57, fontWeight: FontWeight.bold),
|
||
titleLarge: GoogleFonts.roboto(fontSize: 22, fontWeight: FontWeight.w500),
|
||
bodyMedium: GoogleFonts.openSans(fontSize: 14),
|
||
);
|
||
```
|
||
|
||
#### Assets and Images
|
||
* **Image Guidelines:** If images are needed, make them relevant and meaningful,
|
||
with appropriate size, layout, and licensing (e.g., freely available). Provide
|
||
placeholder images if real ones are not available.
|
||
* **Asset Declaration:** Declare all asset paths in your `pubspec.yaml` file.
|
||
|
||
```yaml
|
||
flutter:
|
||
uses-material-design: true
|
||
assets:
|
||
- assets/images/
|
||
```
|
||
|
||
* **Local Images:** Use `Image.asset` for local images from your asset
|
||
bundle.
|
||
|
||
```dart
|
||
Image.asset('assets/images/placeholder.png')
|
||
```
|
||
* **Network images:** Use NetworkImage for images loaded from the network.
|
||
* **Cached images:** For cached images, use NetworkImage a package like
|
||
`cached_network_image`.
|
||
* **Custom Icons:** Use `ImageIcon` to display an icon from an `ImageProvider`,
|
||
useful for custom icons not in the `Icons` class.
|
||
* **Network Images:** Use `Image.network` to display images from a URL, and
|
||
always include `loadingBuilder` and `errorBuilder` for a better user
|
||
experience.
|
||
|
||
```dart
|
||
// When using network images, always provide an errorBuilder.
|
||
Image.network(
|
||
'https://picsum.photos/200/300',
|
||
loadingBuilder: (context, child, progress) {
|
||
if (progress == null) return child;
|
||
return const Center(child: CircularProgressIndicator());
|
||
},
|
||
errorBuilder: (context, error, stackTrace) {
|
||
return const Icon(Icons.error);
|
||
},
|
||
)
|
||
```
|
||
|
||
### UI Theming and Styling Code
|
||
|
||
* **Responsiveness:** Use `LayoutBuilder` or `MediaQuery` to create responsive
|
||
UIs.
|
||
* **Text:** Use `Theme.of(context).textTheme` for text styles.
|
||
* **Text Fields:** Configure `textCapitalization`, `keyboardType`, and
|
||
`placeholder`.
|
||
|
||
### Material Theming Best Practices
|
||
|
||
#### Embrace `ThemeData` and Material 3
|
||
|
||
* **Use `ColorScheme.fromSeed()`:** Use this to generate a complete, harmonious
|
||
color palette for both light and dark modes from a single seed color.
|
||
* **Define Light and Dark Themes:** Provide both `theme` and `darkTheme` to your
|
||
`MaterialApp` to support system brightness settings seamlessly.
|
||
* **Centralize Component Styles:** Customize specific component themes (e.g.,
|
||
`elevatedButtonTheme`, `cardTheme`, `appBarTheme`) within `ThemeData` to
|
||
ensure consistency.
|
||
* **Dark/Light Mode and Theme Toggle:** Implement support for both light and
|
||
dark themes using `theme` and `darkTheme` properties of `MaterialApp`. The
|
||
`themeMode` property can be dynamically controlled (e.g., via a
|
||
`ChangeNotifierProvider`) to allow for toggling between `ThemeMode.light`,
|
||
`ThemeMode.dark`, or `ThemeMode.system`.
|
||
|
||
```dart
|
||
// main.dart
|
||
MaterialApp(
|
||
theme: ThemeData(
|
||
colorScheme: ColorScheme.fromSeed(
|
||
seedColor: Colors.deepPurple,
|
||
brightness: Brightness.light,
|
||
),
|
||
textTheme: const TextTheme(
|
||
displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold),
|
||
bodyMedium: TextStyle(fontSize: 14.0, height: 1.4),
|
||
),
|
||
),
|
||
darkTheme: ThemeData(
|
||
colorScheme: ColorScheme.fromSeed(
|
||
seedColor: Colors.deepPurple,
|
||
brightness: Brightness.dark,
|
||
),
|
||
),
|
||
home: const MyHomePage(),
|
||
);
|
||
```
|
||
|
||
#### Implement Design Tokens with `ThemeExtension`
|
||
|
||
For custom styles that aren't part of the standard `ThemeData`, use
|
||
`ThemeExtension` to define reusable design tokens.
|
||
|
||
* **Create a Custom Theme Extension:** Define a class that extends
|
||
`ThemeExtension<T>` and include your custom properties.
|
||
* **Implement `copyWith` and `lerp`:** These methods are required for the
|
||
extension to work correctly with theme transitions.
|
||
* **Register in `ThemeData`:** Add your custom extension to the `extensions`
|
||
list in your `ThemeData`.
|
||
* **Access Tokens in Widgets:** Use `Theme.of(context).extension<MyColors>()!`
|
||
to access your custom tokens.
|
||
|
||
```dart
|
||
// 1. Define the extension
|
||
@immutable
|
||
class MyColors extends ThemeExtension<MyColors> {
|
||
const MyColors({required this.success, required this.danger});
|
||
|
||
final Color? success;
|
||
final Color? danger;
|
||
|
||
@override
|
||
ThemeExtension<MyColors> copyWith({Color? success, Color? danger}) {
|
||
return MyColors(success: success ?? this.success, danger: danger ?? this.danger);
|
||
}
|
||
|
||
@override
|
||
ThemeExtension<MyColors> lerp(ThemeExtension<MyColors>? other, double t) {
|
||
if (other is! MyColors) return this;
|
||
return MyColors(
|
||
success: Color.lerp(success, other.success, t),
|
||
danger: Color.lerp(danger, other.danger, t),
|
||
);
|
||
}
|
||
}
|
||
|
||
// 2. Register it in ThemeData
|
||
theme: ThemeData(
|
||
extensions: const <ThemeExtension<dynamic>>[
|
||
MyColors(success: Colors.green, danger: Colors.red),
|
||
],
|
||
),
|
||
|
||
// 3. Use it in a widget
|
||
Container(
|
||
color: Theme.of(context).extension<MyColors>()!.success,
|
||
)
|
||
```
|
||
|
||
#### Styling with `WidgetStateProperty`
|
||
|
||
* **`WidgetStateProperty.resolveWith`:** Provide a function that receives a
|
||
`Set<WidgetState>` and returns the appropriate value for the current state.
|
||
* **`WidgetStateProperty.all`:** A shorthand for when the value is the same for
|
||
all states.
|
||
|
||
```dart
|
||
// Example: Creating a button style that changes color when pressed.
|
||
final ButtonStyle myButtonStyle = ButtonStyle(
|
||
backgroundColor: WidgetStateProperty.resolveWith<Color>(
|
||
(Set<WidgetState> states) {
|
||
if (states.contains(WidgetState.pressed)) {
|
||
return Colors.green; // Color when pressed
|
||
}
|
||
return Colors.red; // Default color
|
||
},
|
||
),
|
||
);
|
||
```
|
||
|
||
### Layout Best Practices
|
||
|
||
#### Building Flexible and Overflow-Safe Layouts
|
||
|
||
##### For Rows and Columns
|
||
|
||
* **`Expanded`:** Use to make a child widget fill the remaining available space
|
||
along the main axis.
|
||
* **`Flexible`:** Use when you want a widget to shrink to fit, but not
|
||
necessarily grow. Don't combine `Flexible` and `Expanded` in the same `Row` or
|
||
`Column`.
|
||
* **`Wrap`:** Use when you have a series of widgets that would overflow a `Row`
|
||
or `Column`, and you want them to move to the next line.
|
||
|
||
##### For General Content
|
||
|
||
* **`SingleChildScrollView`:** Use when your content is intrinsically larger
|
||
than the viewport, but is a fixed size.
|
||
* **`ListView` / `GridView`:** For long lists or grids of content, always use a
|
||
builder constructor (`.builder`).
|
||
* **`FittedBox`:** Use to scale or fit a single child widget within its parent.
|
||
* **`LayoutBuilder`:** Use for complex, responsive layouts to make decisions
|
||
based on the available space.
|
||
|
||
#### Layering Widgets with Stack
|
||
|
||
* **`Positioned`:** Use to precisely place a child within a `Stack` by anchoring it to the edges.
|
||
* **`Align`:** Use to position a child within a `Stack` using alignments like `Alignment.center`.
|
||
|
||
#### Advanced Layout with Overlays
|
||
|
||
* **`OverlayPortal`:** Use this widget to show UI elements (like custom
|
||
dropdowns or tooltips) "on top" of everything else. It manages the
|
||
`OverlayEntry` for you.
|
||
|
||
```dart
|
||
class MyDropdown extends StatefulWidget {
|
||
const MyDropdown({super.key});
|
||
|
||
@override
|
||
State<MyDropdown> createState() => _MyDropdownState();
|
||
}
|
||
|
||
class _MyDropdownState extends State<MyDropdown> {
|
||
final _controller = OverlayPortalController();
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return OverlayPortal(
|
||
controller: _controller,
|
||
overlayChildBuilder: (BuildContext context) {
|
||
return const Positioned(
|
||
top: 50,
|
||
left: 10,
|
||
child: Card(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(8.0),
|
||
child: Text('I am an overlay!'),
|
||
),
|
||
),
|
||
);
|
||
},
|
||
child: ElevatedButton(
|
||
onPressed: _controller.toggle,
|
||
child: const Text('Toggle Overlay'),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
```
|
||
|
||
### Color Scheme Best Practices
|
||
|
||
#### Contrast Ratios
|
||
|
||
* **WCAG Guidelines:** Aim to meet the Web Content Accessibility Guidelines
|
||
(WCAG) 2.1 standards.
|
||
* **Minimum Contrast:**
|
||
* **Normal Text:** A contrast ratio of at least **4.5:1**.
|
||
* **Large Text:** (18pt or 14pt bold) A contrast ratio of at least **3:1**.
|
||
|
||
#### Palette Selection
|
||
|
||
* **Primary, Secondary, and Accent:** Define a clear color hierarchy.
|
||
* **The 60-30-10 Rule:** A classic design rule for creating a balanced color scheme.
|
||
* **60%** Primary/Neutral Color (Dominant)
|
||
* **30%** Secondary Color
|
||
* **10%** Accent Color
|
||
|
||
#### Complementary Colors
|
||
|
||
* **Use with Caution:** They can be visually jarring if overused.
|
||
* **Best Use Cases:** They are excellent for accent colors to make specific
|
||
elements pop, but generally poor for text and background pairings as they can
|
||
cause eye strain.
|
||
|
||
#### Example Palette
|
||
|
||
* **Primary:** #0D47A1 (Dark Blue)
|
||
* **Secondary:** #1976D2 (Medium Blue)
|
||
* **Accent:** #FFC107 (Amber)
|
||
* **Neutral/Text:** #212121 (Almost Black)
|
||
* **Background:** #FEFEFE (Almost White)
|
||
|
||
### Font Best Practices
|
||
|
||
#### Font Selection
|
||
|
||
* **Limit Font Families:** Stick to one or two font families for the entire
|
||
application.
|
||
* **Prioritize Legibility:** Choose fonts that are easy to read on screens of
|
||
all sizes. Sans-serif fonts are generally preferred for UI body text.
|
||
* **System Fonts:** Consider using platform-native system fonts.
|
||
* **Google Fonts:** For a wide selection of open-source fonts, use the
|
||
`google_fonts` package.
|
||
|
||
#### Hierarchy and Scale
|
||
|
||
* **Establish a Scale:** Define a set of font sizes for different text elements
|
||
(e.g., headlines, titles, body text, captions).
|
||
* **Use Font Weight:** Differentiate text effectively using font weights.
|
||
* **Color and Opacity:** Use color and opacity to de-emphasize less important
|
||
text.
|
||
|
||
#### Readability
|
||
|
||
* **Line Height (Leading):** Set an appropriate line height, typically **1.4x to
|
||
1.6x** the font size.
|
||
* **Line Length:** For body text, aim for a line length of **45-75 characters**.
|
||
* **Avoid All Caps:** Do not use all caps for long-form text.
|
||
|
||
#### Example Typographic Scale
|
||
|
||
```dart
|
||
// In your ThemeData
|
||
textTheme: const TextTheme(
|
||
displayLarge: TextStyle(fontSize: 57.0, fontWeight: FontWeight.bold),
|
||
titleLarge: TextStyle(fontSize: 22.0, fontWeight: FontWeight.bold),
|
||
bodyLarge: TextStyle(fontSize: 16.0, height: 1.5),
|
||
bodyMedium: TextStyle(fontSize: 14.0, height: 1.4),
|
||
labelSmall: TextStyle(fontSize: 11.0, color: Colors.grey),
|
||
),
|
||
```
|
||
|
||
### Documentation
|
||
|
||
* **`dartdoc`:** Write `dartdoc`-style comments for all public APIs.
|
||
|
||
|
||
#### Documentation Philosophy
|
||
|
||
* **Comment wisely:** Use comments to explain why the code is written a certain
|
||
way, not what the code does. The code itself should be self-explanatory.
|
||
* **Document for the user:** Write documentation with the reader in mind. If you
|
||
had a question and found the answer, add it to the documentation where you
|
||
first looked. This ensures the documentation answers real-world questions.
|
||
* **No useless documentation:** If the documentation only restates the obvious
|
||
from the code's name, it's not helpful. Good documentation provides context
|
||
and explains what isn't immediately apparent.
|
||
* **Consistency is key:** Use consistent terminology throughout your
|
||
documentation.
|
||
|
||
#### Commenting Style
|
||
|
||
* **Use `///` for doc comments:** This allows documentation generation tools to
|
||
pick them up.
|
||
* **Start with a single-sentence summary:** The first sentence should be a
|
||
concise, user-centric summary ending with a period.
|
||
* **Separate the summary:** Add a blank line after the first sentence to create
|
||
a separate paragraph. This helps tools create better summaries.
|
||
* **Avoid redundancy:** Don't repeat information that's obvious from the code's
|
||
context, like the class name or signature.
|
||
* **Don't document both getter and setter:** For properties with both, only
|
||
document one. The documentation tool will treat them as a single field.
|
||
|
||
#### Writing Style
|
||
|
||
* **Be brief:** Write concisely.
|
||
* **Avoid jargon and acronyms:** Don't use abbreviations unless they are widely
|
||
understood.
|
||
* **Use Markdown sparingly:** Avoid excessive markdown and never use HTML for
|
||
formatting.
|
||
* **Use backticks for code:** Enclose code blocks in backtick fences, and
|
||
specify the language.
|
||
|
||
#### What to Document
|
||
|
||
* **Public APIs are a priority:** Always document public APIs.
|
||
* **Consider private APIs:** It's a good idea to document private APIs as well.
|
||
* **Library-level comments are helpful:** Consider adding a doc comment at the
|
||
library level to provide a general overview.
|
||
* **Include code samples:** Where appropriate, add code samples to illustrate usage.
|
||
* **Explain parameters, return values, and exceptions:** Use prose to describe
|
||
what a function expects, what it returns, and what errors it might throw.
|
||
* **Place doc comments before annotations:** Documentation should come before
|
||
any metadata annotations.
|
||
|
||
### Accessibility (A11Y)
|
||
Implement accessibility features to empower all users, assuming a wide variety
|
||
of users with different physical abilities, mental abilities, age groups,
|
||
education levels, and learning styles.
|
||
|
||
* **Color Contrast:** Ensure text has a contrast ratio of at least **4.5:1**
|
||
against its background.
|
||
* **Dynamic Text Scaling:** Test your UI to ensure it remains usable when users
|
||
increase the system font size.
|
||
* **Semantic Labels:** Use the `Semantics` widget to provide clear, descriptive
|
||
labels for UI elements.
|
||
* **Screen Reader Testing:** Regularly test your app with TalkBack (Android) and
|
||
VoiceOver (iOS).
|