Offline-first medication reminder app (MVP) for iOS and Android. Schedule medications, log intakes (taken/skipped), view history. Built with Flutter. Production targets: iOS and Android (mobile). Web build is for demo/development only (no notifications).
- Medications: Add, edit, pause, delete. Daily intake times, repeat interval (e.g. every N days), end condition (never / after X days / after X intakes).
- Notifications: Local notifications with actions (OK, Skip, Postpone 10/30 min) on mobile only.
- Journal: History of intakes grouped by date (Today, Yesterday, MMM d, yyyy).
- Storage: SQLite on mobile; SharedPreferences (localStorage) on web. No sync in MVP.
- UI: Dark theme, Material 3, go_router, Flutter l10n (ARB). English only; structure ready for more locales.
Key UX decisions:
- Swipes (Gmail-style): Swipe left — red background and immediate delete, SnackBar "Undo" (7 s). Swipe right — neutral background and pause for 1 day immediately, SnackBar "Undo". Red is used only for delete (destructive). Overflow menu ⋮ — "Pause for X days" and "Delete permanently" (with confirmation).
- Add flow: Extended FAB "Add", form with Save button at the bottom; time pre-filled with current time for a new medication; focus in the name field when the screen opens. Empty state with CTA "Add first medication".
- History: Grouping by date (Today, Yesterday, MMM d, yyyy). Delete confirmation via bottom sheet with a red action button.
New team onboarding: Read §2 Architecture, §4 How to run, §5 Tests; then CONTRIBUTING.md and test/README.md for setup, style, and test catalog.
- Layers: Domain (entities, no I/O) → Storage (repositories) → App state → UI. UI depends only on
IAppStateand router. - Platform split: Conditional entry (
main.dart→main_io.dart/main_web.dart) and conditional export of concreteAppState(IO vs Web). Web does not pull sqflite or native plugins. - State: Single
IAppState(interface) provided at root via Provider. Implementations:AppState(IO/Web) and testFakeAppState. Screens usecontext.read<IAppState>()andListenableBuilderwhere needed. - Routing: go_router only; paths in
AppRoutes; noNavigator.pushNamedin app code.
Key paths:
lib/domain/— entities (Medication, IntakeLog) and enums.lib/storage/— SQLite (database, repositories) and web (SharedPreferences repositories).lib/notification_scheduler/— mobile notifications and web no-op.lib/app_state_interface.dart— contract for UI and tests;lib/app_state.dart— barrel exporting platformAppState.lib/router/— go_router config andAppRoutes.lib/screens/— full screens and bottom sheets.lib/l10n/— ARB + generated localizations.
- Flutter (SDK ^3.10.8), Provider, go_router, sqflite, path_provider, uuid, flutter_local_notifications, timezone, flutter_timezone, shared_preferences, intl. Dev: flutter_test, integration_test, flutter_lints, mocktail. No deprecated packages.
Prerequisites: Flutter SDK (stable, web enabled) for local run and tests. Docker is optional and runs the same on macOS, Windows, and Linux (Docker Desktop or Engine). For mobile: Xcode (macOS) for iOS; Android SDK for Android.
Using Make (recommended):
| Command | Description |
|---|---|
make deps |
Install dependencies (flutter pub get) |
make run |
Run on connected device or default target |
make run-chrome |
Run web app in Chrome |
make run-chrome-docker |
Build and run web in Docker → http://localhost:8080 |
make build-android |
Build APK |
make build-ios |
Build for iOS |
make test |
Run all unit and widget tests |
make test-unit |
Run unit tests only (domain, storage, router, l10n) |
make test-widget |
Run widget tests only (screens, helpers) |
make test-integration |
Run E2E tests (requires device/emulator) |
make gen-l10n |
Regenerate localizations from ARB |
make analyze |
Run static analysis (lib + test) |
make format |
Format lib and test (run before every commit; CI checks this) |
make check |
Same as CI Lint: analyze + format check |
make check-deps |
Fail if flutter pub get reports "packages have newer versions incompatible with constraints" (same as CI; see test deps_no_outdated_message_test.dart) |
make outdated |
List outdated packages; run periodically to keep deps current (see docs/DEPENDENCIES.md) |
make clean |
Clean build artifacts |
Docker (no Flutter required on host): From project root run make run-chrome-docker or cd docker && docker compose up --build. Open http://localhost:8080. The same image runs on macOS, Windows, and Linux.
Without Make: flutter pub get, flutter run, flutter run -d chrome, flutter build apk / flutter build ios, flutter test, flutter analyze lib test, and for E2E: flutter drive --driver=test_driver/integration_test.dart --target=integration_test/app_test.dart.
- Unit:
test/domain/,test/storage/,test/router/,test/l10n/— domain models, serialization, route paths, localization strings. Alsotest/format_check_test.dart(code must be formatted) andtest/deps_no_outdated_message_test.dart(no "packages have newer versions" message); if these fail, runmake formator fix dependencies (see CONTRIBUTING). - Widget:
test/screens/,test/helpers/— screens withFakeAppState(implementsIAppState) and Provider. - E2E:
integration_test/app_test.dart— full app launch; run withmake test-integration(orflutter drive ...). Requires device/emulator. E2E is not run in CI; run locally before release or for smoke checks.
A plain-language description of what each test file and test does is in test/README.md (for QA and new developers).
How to add tests:
- Unit: New file
test/<layer>/<name>_test.dart; usetest()/expect()frompackage:flutter_test/flutter_test.dart. - Widget: Use
testWidgets(),pumpWidget()withMaterialAppandChangeNotifierProvider<IAppState>.value(value: FakeAppState(), ...), thenfind.text(),tester.tap(), etc. - E2E: Add or edit under
integration_test/; ensureIntegrationTestWidgetsFlutterBinding.ensureInitialized()and run the app (e.g.app.main()), thenpumpAndSettle()and assert on UI.
- Where it runs: The repo uses GitHub Actions (
.github/workflows/ci.yml) on push and PR tomain/master. The pipeline is generic (checkout → setup → lint → test → build); the same steps can be run in any CI (GitLab, Jenkins, etc.) using the commands below. Android job requires a runner with Android SDK (e.g. GitHub-hosted ubuntu-latest); the workflow runsflutter doctor --android-licensesbefore build. - Jobs (all must pass):
- Lint —
flutter analyze lib testanddart format --set-exit-if-changed lib test(if any file is unformatted, Lint fails; runmake formatlocally, thenmake checkto verify) - Unit tests —
flutter test test/domain test/storage test/router test/l10n - Widget tests —
flutter test test/screens test/helpers - Build Web —
flutter build web --release(after lint + tests) - Build Android APK —
flutter build apk --release(after lint + tests)
- Lint —
- Reproducible: Same commands locally:
make analyze,make test-unit,make test-widget,make run-chrome-dockerorflutter build apk. No secrets required for CI. - Rule: If any job fails, do not merge until fixed.
- Why Build Android can fail: (1) Old plugins used Flutter V1 embedding (
Unresolved reference: Registrar). (2) Some plugins require Android Gradle config (e.g. core library desugaring forflutter_local_notifications); if missing, the build fails. Native/Gradle issues are not caught by unit/widget tests; only the Build Android APK CI job catches them. Before adding or upgrading native plugins, see CONTRIBUTING §9 and docs/DEPENDENCIES.md (including §3 Android native setup).
SDLC: Feature/bugfix → branch → implement + tests → PR → review → merge to main. Release from main (tag when needed).
Branching: Trunk-based. Main branch is always releasable. Short-lived feature branches; no long-lived release branches. Rationale: single app, small team, MVP; avoids merge hell and keeps deploy path simple.
Code review: Every change via PR. Review checks: lint and tests pass, no unrelated edits, public API and behavior documented where non-obvious.
Definition of Done: Lint passes (flutter analyze lib test); unit/widget tests added/updated for new behavior; README/docs updated if user-facing or process changes; no known regressions.
Test strategy:
- Unit: Domain and pure helpers (serialization, route constants); fast, no Flutter shell.
- Widget: Screens and key flows with
FakeAppState; cover main UX paths. - E2E: Smoke (app launches, main screen visible); optional deeper flows on stable device/emulator.
- Coverage: Aim for meaningful coverage on domain and repositories; no hard gate in CI yet.
Releases: Version in pubspec.yaml (version: 1.0.0+1). For stores: tag in git, build from tag, upload to App Store Connect / Play Console. No automated store deploy in MVP.
- Single global state (IAppState): Keeps MVP simple; no Redux/Bloc. Plan to extract use cases if the app grows.
- Conditional entry and platform export: Keeps web build free of sqflite and native plugins; tree-shaking and smaller web bundle.
- IAppState interface: Lets widget tests inject
FakeAppStatewithout platform-specific code. - go_router: Declarative routes and deep links; single source of truth for paths (
AppRoutes). - SQLite (mobile) / SharedPreferences (web): No backend in MVP; local-first. Sync and migration strategy deferred.
- Web: No notifications; storage only. Demo/development only.
- Notifications: Auto-postpone after 10 minutes not implemented (schedule follow-up at T+10 when user does not act; see Backlog). Max 50 scheduled per medication.
- DB migrations: Schema version 1 only; no migration path yet. Add migrations before changing schema.
- Web storage: Full lists in SharedPreferences; may hit size limits with large history; no pagination.
- Accessibility: Contrast and screen-reader support not fully validated.
lib/
├── main.dart, main_io.dart, main_web.dart # Entry: main_io (mobile), main_web (web)
├── app_state_interface.dart # Contract for UI and tests
├── app_state.dart # Barrel (exports platform AppState)
├── app_state_io.dart, app_state_web.dart
├── domain/ # medication, intake_log, domain.dart
├── storage/ # database, repositories, storage_web
├── notification_scheduler/ # mobile + web stub
├── router/ # app_router.dart, AppRoutes
├── screens/ # list, edit, history, settings, pause sheet
└── l10n/ # ARB + generated
test/ # unit, widget, helpers
integration_test/ # E2E
docker/ # Dockerfile + compose for web
Prioritization: High = user impact or risk; Medium = clear value, not blocking; Low = nice-to-have or tech debt when capacity allows.
| Item | Description | User value | Tech effort | Priority |
|---|---|---|---|---|
| Auto-postpone 10 min | After scheduled time, show reminder again in 10 min without logging (schedule follow-up at T+10; cancel if user taps main notification). | Fewer missed intakes. | Medium (background/plugin constraints). | High |
| DB migrations | Version schema and add migration steps before changing tables. | Safe updates, no data loss. | Low. | High |
| Accessibility pass | Semantic labels, touch targets, contrast (WCAG). | Usable for screen readers and low vision. | Medium. | Medium |
| Settings: theme/language | Real theme toggle and language selector (when more locales exist). | User control. | Medium. | Low |
| Pagination / archive (web) | Limit or archive old intake logs in SharedPreferences. | Avoid storage limits on web. | Medium. | Low |
| Analytics (opt-in) | Events for add/edit/pause/delete and intake actions; no PII. | Product insights. | Medium. | Low |
| Sync (future) | Optional cloud backup/sync with auth. | Multi-device, backup. | High. | Low (post-MVP) |
| Security checklist | Document encryption at rest, permissions, data handling. | Compliance and trust. | Low. | Medium |
- Contributing: See CONTRIBUTING.md for setup, run, test, code style, and PR process.
- Dependencies and native plugins: docs/DEPENDENCIES.md — why "Registrar" build failures happen and how to avoid them when upgrading.
- Flutter: flutter.dev
- Effective Dart: dart.dev/guides/language/effective-dart