# Pitfalls Research **Domain:** Local-first Flutter household chore management app (Android, Riverpod + Drift) **Researched:** 2026-03-15 **Confidence:** HIGH (Riverpod/Drift specifics), MEDIUM (scheduling edge cases), HIGH (Android notification permissions) --- ## Critical Pitfalls ### Pitfall 1: Drift Schema Changes Without Incrementing `schemaVersion` **What goes wrong:** A developer modifies a table definition (adds a column, renames a field, adds a new table) but forgets to increment `schemaVersion` and write a corresponding `onUpgrade` migration. On fresh installs, `onCreate` runs cleanly and everything works. On existing installs, the app opens against the old schema — queries silently return wrong data or crash with column-not-found errors. This is especially dangerous because it only surfaces after shipping an update to a real device. **Why it happens:** Drift's code generation regenerates Dart classes correctly based on the new table definition, so the app compiles and runs fine in development (developer always uses a fresh database). The migration gap is invisible until the app is installed over an older version. **How to avoid:** - Before making any schema change, run `dart run drift_dev make-migrations` to snapshot the current schema. Drift generates a migration test file you can run to verify correctness. - Treat `schemaVersion` as a checklist item: every PR that touches a table definition must bump the version and add a `stepByStep` migration block. - Use Drift's `stepByStep` API rather than a raw `if (from < N)` block — it generates per-step migration scaffolding that reduces errors. - Test migrations with `dart run drift_dev schema verify` against the generated schema snapshots. **Warning signs:** - A table definition file changed in a commit but `schemaVersion` did not change. - Tests pass on CI (fresh DB) but crash reports appear from field users after an update. - `SqliteException: no such column: X` errors in crash logs. **Phase to address:** Foundation phase (database setup). Establish the `make-migrations` workflow before writing the first table. --- ### Pitfall 2: "Next Due Date" Calculated from Wall-Clock `DateTime.now()` Without Timezone Anchoring **What goes wrong:** Recurring tasks use an interval (e.g., "every 7 days"). When a task is marked complete, the next due date is computed as `completionTime + Duration(days: interval)`. Because `DateTime.now()` returns UTC or local time depending on context, and because the notion of "today" is timezone-sensitive, two bugs emerge: 1. **Completion near midnight:** A task completed at 11:58 PM lands on a different calendar day than one completed at 12:02 AM. The next due date shifts by one day unpredictably. 2. **"Every N days" vs. "after completion":** Without a clear policy, the schedule drifts — a weekly vacuum scheduled for Monday slowly becomes a Wednesday vacuum because the user always completes it a day late. **Why it happens:** Developers store `DateTime` values directly from `DateTime.now()` without thinking about whether "due today" means "due before end of local calendar day" or "due within 24 hours." SQLite stores timestamps as integers (Unix epoch) — no timezone metadata is preserved. **How to avoid:** - Store all due dates as `Date` (calendar day only — year/month/day), not `DateTime`. For a chore app, a task is due "on a day," not "at a specific second." - Use `DateTime.now().toLocal()` explicitly and strip time components when computing next due dates: `DateTime(now.year, now.month, now.day + interval)`. - Define a policy at project start: "every N days from last completion date" (rolling) vs. "every N days from original start date" (fixed anchor). HouseHoldKeaper's core value — "trust the app to schedule the next occurrence" — implies rolling is correct, but document this explicitly. - Store `lastCompletedDate` as a calendar date, not a timestamp, so the next due date calculation is always date arithmetic, never time arithmetic. **Warning signs:** - Due dates drifting week-by-week when a user is consistently slightly early or late on completions. - "Overdue" tasks appearing that were completed the previous evening. - Unit tests for due date logic only testing noon completions, never midnight edge cases. **Phase to address:** Core task/scheduling phase. Write unit tests for due date calculation before wiring it to the UI. --- ### Pitfall 3: Android Notification Permissions Not Requested at Runtime (API 33+) **What goes wrong:** The app targets Android 13+ (API 33). `POST_NOTIFICATIONS` is a runtime permission — notifications are **off by default** for new installs. The app ships, the daily summary notification never fires, and users see nothing without knowing why. Additionally, `SCHEDULE_EXACT_ALARM` is denied by default on API 33+, so scheduled notifications (even if the permission is declared in the manifest) silently fail to fire. **Why it happens:** Developers coming from pre-API 33 experience assume notification permission is implicitly granted. The manifest declaration is necessary but not sufficient — a runtime `requestPermission()` call is required before `POST_NOTIFICATIONS` works on API 33+. **How to avoid:** - Declare `POST_NOTIFICATIONS`, `SCHEDULE_EXACT_ALARM`, and `RECEIVE_BOOT_COMPLETED` in `AndroidManifest.xml`. - Use `flutter_local_notifications`'s `.requestNotificationsPermission()` method on Android at an appropriate moment (first launch or when the user enables notifications in settings). - Always call `canScheduleExactAlarms()` before scheduling; if denied, guide the user to Settings → Special app access → Alarms. - Use `AndroidScheduleMode.exactAllowWhileIdle` for all scheduled notifications to ensure delivery during Doze mode. - Call `tz.initializeTimeZones()` at app startup — missing this is a silent bug that causes scheduled notification times to be wrong. - Register `ScheduledNotificationBootReceiver` in the manifest to reschedule notifications after device reboot. **Warning signs:** - Notification permission prompt never appears during testing on a real API 33 device. - Scheduled test notifications arrive correctly on emulator (which has fewer battery restrictions) but not on a physical device. - `flutter_local_notifications` returns success from the schedule call but no notification fires. **Phase to address:** Notifications phase. Test on a physical API 33+ Android device, not just the emulator, before marking notifications done. --- ### Pitfall 4: Riverpod `ref.watch` / `ref.listen` Used Outside `build` **What goes wrong:** Calling `ref.watch(someProvider)` inside `initState`, a button callback, or an async function causes either a runtime exception ("Cannot call watch outside a build-scope") or silent stale reads (the widget never rebuilds when the watched provider changes). Similarly, calling `ref.listen` inside `initState` instead of using `ref.listenManual` results in the listener being lost after the first rebuild. **Why it happens:** The distinction between `ref.watch` (for reactive rebuilds inside `build`) and `ref.read` (for one-time reads in callbacks) is non-obvious to Riverpod newcomers. The PROJECT.md acknowledges the developer is new to Drift — this likely extends to Riverpod patterns as well. **How to avoid:** - Rule of thumb: `ref.watch` **only** inside `build()`. `ref.read` in callbacks, `initState`, event handlers. `ref.listen` in `build()` for side effects (showing SnackBars). `ref.listenManual` in `initState` or outside build. - Enable `riverpod_lint` rules — they catch `ref.watch` outside `build` at analysis time, before runtime. - Prefer `@riverpod` code generation (riverpod_generator) over manually constructing providers — the annotation API reduces the surface area for these misuse patterns. - Do not call `ref.read(autoDisposeProvider)` in `initState` to "warm up" a provider — the auto-dispose mechanism may discard the state before any widget watches it. **Warning signs:** - "Cannot use ref functions after the widget was disposed" exceptions in logs. - UI not updating when underlying provider state changes, even though data changes are confirmed in the database. - `ConsumerStatefulWidget` screens that call `ref.watch` in `initState`. **Phase to address:** Foundation phase (project structure and state management setup). Establish linting rules before writing feature code. --- ### Pitfall 5: `autoDispose` Providers Losing State on Screen Navigation **What goes wrong:** Riverpod's code-generation (`@riverpod`) enables `autoDispose` by default. When a user navigates away from the task list and returns, the provider state is discarded and rebuilt from scratch — triggering a full database reload, causing visible loading flicker, and losing any in-flight UI state (e.g., a sort selection, scroll position). For a household app used daily, this is a recurring annoyance. **Why it happens:** `autoDispose` disposes provider state when no listeners remain (i.e., when the widget leaves the tree). Developers using code generation often don't realize `autoDispose` is the default and that persistent UI state requires opt-in `keepAlive`. **How to avoid:** - Use `@Riverpod(keepAlive: true)` for long-lived app-wide state: room list, task list, daily plan view. These should persist for the entire app session. - Use default `autoDispose` only for transient UI state that should be discarded (e.g., form state for a "new task" dialog). - For async providers that load database content, use `ref.keepAlive()` after the first successful load to prevent re-querying on every navigation. - Drift's `.watch()` streams already provide reactive updates — no need to aggressively dispose and recreate the Riverpod layer; let Drift push updates. **Warning signs:** - Noticeable loading flash every time the user returns to the main screen. - Database queries firing more often than expected (visible in logs). - Form state (partially filled "edit task" screen) resetting if the user briefly leaves. **Phase to address:** Core UI phase. Define provider lifetime policy at architecture setup before building feature providers. --- ## Technical Debt Patterns Shortcuts that seem reasonable but create long-term problems. | Shortcut | Immediate Benefit | Long-term Cost | When Acceptable | |----------|-------------------|----------------|-----------------| | Storing due dates as `DateTime` (with time) instead of `Date` (date only) | Less refactoring of existing patterns | Midnight edge cases, overdue logic bugs, timezone drift | Never — use date-only from day one | | Skipping Drift `make-migrations` workflow for early schemas | Faster iteration in early development | First schema change on a shipped app causes data loss | Only acceptable before first public release; must adopt before any beta | | Hardcoding color values instead of using `ColorScheme.fromSeed()` | Faster during prototyping | Broken dark mode, inconsistent tonal palette, future redesign pain | Prototyping only; replace before first feature-complete build | | Using `ref.read` everywhere instead of `ref.watch` | No accidental rebuilds | UI never updates reactively; state bugs that look like data bugs | Never in production widgets | | Scheduling a notification for every future recurrence at task creation | Simpler code path | Hits Samsung's 500-alarm limit; stale notifications after task edits; manifest bloat | Never — schedule only the next occurrence | | Skipping `riverpod_lint` to avoid setup overhead | Faster start | Silent provider lifecycle bugs accumulate; harder to catch without the linter | Never | --- ## Integration Gotchas Common mistakes when integrating the core libraries. | Integration | Common Mistake | Correct Approach | |-------------|----------------|------------------| | `flutter_local_notifications` + Android 13 | Declaring permission in manifest only | Also call `requestNotificationsPermission()` at runtime; check `canScheduleExactAlarms()` | | `flutter_local_notifications` + scheduling | Using `DateTime.now()` without timezone init | Call `tz.initializeTimeZones()` at startup; use `TZDateTime` for all scheduled times | | `flutter_local_notifications` + reboot | Not registering `ScheduledNotificationBootReceiver` | Register receiver in `AndroidManifest.xml`; reschedule on boot | | Drift + `build_runner` | Running `build_runner build` once then forgetting | Use `build_runner watch` during development; always re-run after table changes | | Drift + migrations | Adding columns without `addColumn()` migration | Always pair `schemaVersion` bump with explicit migration step; use `stepByStep` API | | Riverpod + Drift | Watching a Drift `.watch()` stream inside a Riverpod `StreamProvider` | This is correct — but ensure the `StreamProvider` has `keepAlive: true` to avoid stream teardown on navigation | | Riverpod + `@riverpod` codegen | Assuming `@riverpod` is equivalent to `@Riverpod(keepAlive: false)` | It is — explicitly add `keepAlive: true` for providers that must survive navigation | --- ## Performance Traps Patterns that work at small scale but cause UX issues as data grows. | Trap | Symptoms | Prevention | When It Breaks | |------|----------|------------|----------------| | Loading all tasks into memory, then filtering in Dart | Fine with 20 tasks, slow with 200 | Push filter/sort logic into Drift SQL queries with `.where()` and `orderBy` | Around 100-200 tasks when filtering involves date math | | Rebuilding the entire task list widget on any state change | Imperceptible with 10 items, janky with 50+ | Use `select` on providers to narrow rebuilds; use `ListView.builder` (never `Column` with mapped list) | 30-50 tasks in a scrollable list | | Storing room photos as raw bytes in SQLite | Works for 1-2 photos, degrades quickly | Store photos as file paths in the document directory; keep only the path in the database | After 3-4 high-res room photos | | Scheduling a daily notification with an exact `TZDateTime` far into the future | Works initially; notification becomes stale after task edits or completions | Schedule only the next occurrence; reschedule in the task completion handler | After the first task edit that changes the due date | --- ## UX Pitfalls Common UX mistakes specific to recurring task / chore management apps. | Pitfall | User Impact | Better Approach | |---------|-------------|-----------------| | No distinction between "due today" and "overdue" in the daily plan | User cannot triage quickly — everything looks equally urgent | Separate sections: Overdue (red tint), Due Today (normal), Upcoming (muted). PROJECT.md already specifies this — do not flatten it during implementation. | | Marking a task complete immediately removes it from today's view | User cannot see what they've done today — feels like the work disappeared | Show completed tasks in a "Done today" collapsed section or with a strikethrough for the session | | Cleanliness indicator updating in real time during rapid completions | Flickering percentage feels wrong | Debounce indicator updates or recalculate only on screen focus, not per-completion | | Scheduling next due date from the moment the completion button is tapped | A task completed late at 11 PM schedules the next occurrence for 11 PM in N days — misaligned with calendar days | Compute next due date from the calendar date of completion, not the timestamp | | Notification fires while the user has the app open | Duplicate information; notification feels spammy | Cancel or suppress the daily summary notification if the app is in the foreground at scheduled time | | German-only strings hardcoded as literals scattered throughout widget code | Impossible to add English localization later without full-codebase surgery | Use Flutter's `l10n` infrastructure (`AppLocalizations`) from the start, even with only one locale — adding a second locale later is then trivial | --- ## "Looks Done But Isn't" Checklist Things that appear complete but are missing critical pieces. - [ ] **Notifications:** Tested on a real Android 13+ device (not emulator) with a fresh app install — permission prompt appears, notification fires at scheduled time. - [ ] **Notifications after reboot:** Device rebooted after scheduling a notification — notification still fires at the correct time. - [ ] **Drift migrations:** App installed over an older APK (not fresh install) — data survives, no crash, no missing columns. - [ ] **Due date calculation:** Unit tests cover completion at 11:58 PM, 12:02 AM, and on the last day of a short month (e.g., Feb 28/29). - [ ] **Overdue logic:** Tasks not completed for multiple intervals show as overdue for the correct number of days, not just "1 day overdue." - [ ] **Cleanliness indicator:** Rooms with no tasks do not divide by zero. - [ ] **Task deletion:** Deleting a task cancels its scheduled notification — no orphaned alarms in the AlarmManager queue. - [ ] **Localization:** All user-visible strings come from `AppLocalizations`, not hardcoded German literals. - [ ] **Dark mode:** Every screen renders correctly in both light and dark theme — test on device, not just the Flutter preview. - [ ] **Task with no recurrence interval:** One-time tasks (if supported) do not trigger infinite "overdue" state after completion. --- ## Recovery Strategies When pitfalls occur despite prevention, how to recover. | Pitfall | Recovery Cost | Recovery Steps | |---------|---------------|----------------| | Shipped without migration; users lost data | HIGH | Ship a hotfix that detects the broken schema version, performs a safe re-creation with sane defaults, and notifies the user their task history was reset | | Due dates drifted due to timestamp vs. date bug | MEDIUM | Write a one-time migration that normalizes all stored due dates to date-only (midnight local time); bump schemaVersion | | Notification permission never requested; users have no notifications | LOW | Add permission request on next app launch; show an in-app banner explaining why | | Hardcoded German strings throughout widgets | HIGH | Requires systematic extraction to ARB files; no shortcut — this is a full codebase refactor | | `autoDispose` caused state loss and data reload on every navigation | MEDIUM | Add `keepAlive: true` to affected providers; test all navigation flows after the change | | Notification icon stripped by R8 | LOW | Add `keep.xml` proguard rule; rebuild and redeploy | --- ## Pitfall-to-Phase Mapping How roadmap phases should address these pitfalls. | Pitfall | Prevention Phase | Verification | |---------|------------------|--------------| | Drift schema without migration workflow | Foundation (DB setup) | Run `drift_dev schema verify` passes; migration test file exists | | Due date timestamp vs. date bug | Core scheduling logic | Unit tests for midnight, month-end, and interval edge cases all pass | | Android notification runtime permissions | Notifications phase | Fresh install on API 33+ physical device shows permission prompt; notification fires | | Notification lost after reboot | Notifications phase | Device rebooted; notification rescheduled and fires at correct time | | `ref.watch` outside `build` | Foundation (project setup) | `riverpod_lint` enabled; no lint warnings in initial project scaffold | | `autoDispose` state loss on navigation | Core UI phase | Navigate away and back 3 times; no loading flash, no database re-query | | Hardcoded German strings | Foundation (project setup) | `l10n.yaml` and `AppLocalizations` set up before first UI widget is written | | Notification icon stripped by R8 | Notifications phase | Release build tested (not just debug); notification icon renders correctly | | Overdue logic / cleanliness indicator divide-by-zero | Daily plan view phase | Empty room (0 tasks) renders without crash; indicator shows neutral state | | Room photo stored as blob | Room management phase | Photos stored as file paths; database size stays small after adding 5 rooms with photos | --- ## Sources - [Drift official migration docs](https://drift.simonbinder.eu/migrations/) — schema versioning and `stepByStep` API - [Riverpod official docs — Refs](https://riverpod.dev/docs/concepts2/refs) — `ref.watch` vs `ref.read` vs `ref.listen` semantics - [Riverpod official docs — Auto Dispose](https://riverpod.dev/docs/concepts2/auto_dispose) — `autoDispose` lifecycle - [flutter_local_notifications pub.dev](https://pub.dev/packages/flutter_local_notifications) — Android permission requirements and migration notes - [Android Developer docs — Notification permission](https://developer.android.com/develop/ui/views/notifications/notification-permission) — `POST_NOTIFICATIONS` runtime permission - [Android Developer docs — Schedule exact alarms (API 14)](https://developer.android.com/about/versions/14/changes/schedule-exact-alarms) — `SCHEDULE_EXACT_ALARM` denied by default on API 33+ - [Andrea Bizzotto — Riverpod data caching and provider lifecycle](https://codewithandrea.com/articles/flutter-riverpod-data-caching-providers-lifecycle/) — `keepAlive`, `autoDispose` patterns - [Flutter Material 3 migration guide](https://docs.flutter.dev/release/breaking-changes/material-3-migration) — widget replacements and theming changes - [Common SQLite mistakes — Sparkleo/Medium](https://medium.com/@sparkleo/common-sqlite-mistakes-flutter-devs-make-and-how-to-avoid-them-1102ab0117d5) — N+1 queries, transactions, normalization - [ha-chore-helper GitHub](https://github.com/bmcclure/ha-chore-helper) — "every" vs "after" scheduling modes in chore apps, midnight edge cases - [Drift Local Database Part 1 — Medium](https://r1n1os.medium.com/drift-local-database-for-flutter-part-1-intro-setup-and-migration-09a64d44f6df) — `schemaVersion` and migration workflow for new developers --- *Pitfalls research for: Local-first Flutter household chore management app (HouseHoldKeaper)* *Researched: 2026-03-15*