Lightweight architecture decision records. Each: context → decision → rationale → alternatives rejected. Newest decisions can be appended over time.
- Decision: Build Ponvia in Flutter (Dart), single codebase for Android + iOS.
- Rationale: One codebase for both stores; excellent performance; strong local-DB and charting ecosystem; developer targeting Android now, iOS later without a rewrite.
- Rejected: Native Kotlin/Swift (two codebases), React Native (weaker for smooth charts/perf here), KMP+Compose (iOS UI still bespoke).
- Decision:
flutter_riverpodwith code-gen (riverpod_generator). - Rationale: Compile-safe, testable without a widget tree, fine-grained rebuilds (matters for the performance goal), first-class async/stream providers that pair well with Drift's reactive queries.
- Rejected: Bloc (more boilerplate for this scale),
provider(less safety), setState-only (won't scale across features).
- Decision: Drift over SQLite for domain data (weights, goals).
- Rationale: Robust time-series queries and indexing for history/charts; reactive
watchstreams for live UI; first-class migrations; trivial to serialize the whole DB to a JSON backup and to emit CSV; leaves a clean path to add calorie tables later. - Rejected: Isar (fast but maintenance/roadmap uncertainty), Hive (weak querying for time-series + goals), raw sqflite (more manual, less type-safety).
- Decision: Scalar settings (locale, theme, unit, onboarding flag, reminder config)
in
shared_preferences; domain data in Drift. - Rationale: Settings are small key/values read at startup; no need for a DB round-trip or schema. Keeps the DB focused on records.
- Rejected: Storing settings in Drift (heavier for scalars), platform-specific stores.
- Decision:
go_routerfor declarative, deep-link-capable routing. - Rationale: Clean onboarding redirect gate; notification taps deep-link to
/log; URL-style routes ease testing and future web smoke tests. - Rejected: Navigator 1.0 (imperative, awkward for redirect gating), auto_route (extra codegen without clear benefit here).
- Decision: Flutter's built-in
gen_l10nwithapp_en.arb(template) +app_da.arb. - Rationale: Official, no runtime dependency, compile-checked keys; Danish + English are the launch languages; locale overrides system when the user chooses.
- Rejected:
easy_localization/slang(extra deps; built-in is sufficient here).
- Decision:
fl_chartfor the weight trend (mini + full). - Rationale: Flexible, performant line charts; themeable to match design tokens; widely used and maintained.
- Rejected:
syncfusion_flutter_charts(heavier/licensing), hand-rolledCustomPainter(more effort; revisit only if fl_chart limits us).
- Decision:
flutter_local_notificationswithtimezone+flutter_timezonefor zoned, scheduled local reminders. - Rationale: Purely local (no push server needed), supports daily/weekly/monthly scheduling in the device timezone, handles Android 13+ permission and exact alarms.
- Rejected: Firebase Cloud Messaging (requires network/cloud; violates local-only), workmanager-only (less suited to precise user-facing reminders).
- Decision: Store all weights in kg; convert for display to kg/lb/st.
- Rationale: One canonical unit avoids drift/rounding bugs; switching display units never mutates stored data; conversions are pure and unit-tested.
- Rejected: Storing in the user's current display unit (fragile on unit change).
- Decision: Export/import a versioned JSON backup (weights + goals + settings) and a CSV of weight history.
- Rationale: JSON gives lossless backup/restore; CSV gives spreadsheet interoperability. Versioned envelope enables safe migration. Matches user choice.
- Rejected: JSON-only (no spreadsheet path), CSV-only (loses goals/settings fidelity), proprietary/binary formats (not portable).
- Decision: Package/bundle id
io.github.kupperlupperdupper.ponvia. - Rationale: The developer owns no domain; the community convention for a
GitHub-hosted project is reverse-DNS on
github.com/<user>→io.github.<user>. Lowercased per Android/iOS id rules. Stable and unlikely to collide. - Rejected:
com.littlebeacon.*(explicitly declined by the user),com.github.*(discouraged;github.comisn't the publisher), a made-up domain.
- Decision: A GitHub Actions workflow triggered on
v*tags (and manual dispatch) builds a release APK, generates a changelog from commits since the previous tag (augmented byCHANGELOG.mdwhen present), generates a QR code encoding the APK's release-asset download URL, and publishes a GitHub Release with the APK + QR attached and the QR inlined in the notes for phone scanning. - Rationale: The developer installs directly on their own Android device; a scannable QR to a hosted APK is the lowest-friction sideload path. Deterministic asset URLs let the QR be generated in the same run. A single universal APK (not split-per-ABI) means one file and one QR to scan.
- Signing: v1 uses Flutter's default release signing (debug keystore) so the APK installs without extra setup. A real upload keystore via repo secrets can be added later without changing the workflow's shape.
- Rejected: Split-per-ABI APKs (multiple files/QRs), Play Internal Testing (heavier, account/setup overhead for a personal app), third-party QR web services (network + privacy; QR is generated on the runner instead).
- Dependency note: The workflow references
flutter build apk, so it only succeeds once the M1 scaffold (pubspec.yaml+ Android project) exists and av*tag is pushed. Until then it is inert.
- Decision: Use hand-written Riverpod providers (
Provider,NotifierProvider,StreamProvider) instead ofriverpod_generator. - Rationale: At M1,
riverpod_generator(analyzer ^13) could not co-resolve withdrift_dev(analyzer <3 transitively via the test toolchain) — version solving failed. Drift's codegen is mandatory; Riverpod's is optional. The non-codegen API is fully supported in Riverpod 3.x and providers are an implementation detail, so this is reversible later without reworking call sites. Supersedes the codegen mention in ARCHITECTURE §4 / ADR-002. - Rejected: Pinning older Riverpod/analyzer (would hold back Drift), dropping Drift codegen (impossible).
- Decision: Set
kotlin.incremental=falseinandroid/gradle.properties. - Rationale:
compileDebugKotlinrepeatedly crashed with "Could not close incremental caches … *.tab" on this Windows dev box (file-locking onbuild/, likely AV). Disabling incremental compilation is slightly slower but builds reliably. Harmless on CI. - Rejected: Excluding
build/from AV (not always possible), retрy loops (didn't help).
- Decision: Use
file_selector(import/open) andshare_plus(export via the OS share sheet) for data portability. Notfile_picker. - Rationale:
file_pickerapplies the Kotlin Gradle Plugin, which fails to compile under Flutter's new Built-in-Kotlin Android setup used by this project (compileDebugKotlin not found in project ':file_picker'→GeneratedPluginRegistrantcan't resolveFilePickerPlugin).file_selector(Flutter-team, Java-based) andshare_plusdon't apply KGP and build cleanly. Export as a share also gives a nicer "save to…/send" flow than a bare save dialog.file_pickeralso conflicted withshare_pluson win32. - Rejected:
file_picker(build failure), keepingshare_plus+file_picker(win32 conflict), writing exports only to app storage (poor discoverability).
- Context: Flagged as a thing to keep in view for the future. The EU AI Act (Regulation (EU) 2024/1689) regulates AI systems — machine-based systems that, for explicit or implicit objectives, infer from input how to generate outputs such as predictions, content, recommendations, or decisions (Art. 3(1)).
- Decision: As of this writing Ponvia contains no AI system, so the AI Act imposes no obligations on it. We note the fact here rather than adding any compliance machinery. Health data handling is covered by our local-only/privacy posture (see the Privacy page and the app's no-network design), which is a GDPR matter, not an AI-Act one.
- Rationale: Every "smart-looking" feature in Ponvia is deterministic maths, not inference: the EMA trend-weight line, BMI, the goal-ETA projection, and the closest-goal selection are fixed formulas/statistics with no learned model and no training. They produce the same output for the same input every time and are not AI systems under Art. 3(1).
- Reassess before shipping any of these (each introduces an AI system and re-opens
the analysis — do a fresh review, and get legal sign-off; this note is engineering
guidance, not legal advice):
- AI/ML food or calorie recognition (photo → food/calories), or any learned calorie/nutrition estimation model — the most likely trigger given the calorie roadmap.
- Predictive health insights or recommendations derived from a trained model.
- An LLM/assistant or any generative feature. If a user interacts with an AI system or sees AI-generated content, the transparency duties (Art. 50) apply — disclose it clearly.
- Anything making a health/medical claim, diagnosis, or treatment suggestion — that can pull the feature toward high-risk classification and also engages medical-device rules (MDR); treat as a hard stop pending review. Keep this ADR current when calorie tracking or any model-backed feature is designed.