From f84bd01bef88a5e60b71bd66ee3f454a0be1ee68 Mon Sep 17 00:00:00 2001 From: Jean-Luc Makiola Date: Sun, 15 Mar 2026 21:30:31 +0100 Subject: [PATCH] docs(phase-2): research rooms and tasks domain --- .../phases/02-rooms-and-tasks/02-RESEARCH.md | 804 ++++++++++++++++++ 1 file changed, 804 insertions(+) create mode 100644 .planning/phases/02-rooms-and-tasks/02-RESEARCH.md diff --git a/.planning/phases/02-rooms-and-tasks/02-RESEARCH.md b/.planning/phases/02-rooms-and-tasks/02-RESEARCH.md new file mode 100644 index 0000000..7b877d2 --- /dev/null +++ b/.planning/phases/02-rooms-and-tasks/02-RESEARCH.md @@ -0,0 +1,804 @@ +# Phase 2: Rooms and Tasks - Research + +**Researched:** 2026-03-15 +**Domain:** Flutter CRUD features with Drift ORM, Riverpod 3 state management, scheduling logic +**Confidence:** HIGH + +## Summary + +Phase 2 is the first feature-heavy phase of HouseHoldKeaper. It transforms the empty shell from Phase 1 into a functional app where users create rooms, add tasks with recurrence schedules, mark tasks done, and see overdue indicators. The core technical domains are: (1) Drift table definitions with schema migration from v1 to v2, (2) Riverpod 3 providers wrapping Drift stream queries for reactive UI, (3) date arithmetic for calendar-anchored and day-count recurrence scheduling, (4) drag-and-drop reorderable grid for room cards, and (5) a template data system for German-language task presets. + +The existing codebase establishes clear patterns: `@riverpod` code generation with `Ref` (not old generated ref types), `AppDatabase` with `NativeDatabase.memory()` for tests, GoRouter `StatefulShellRoute` with nested routes, Material 3 theming via `ColorScheme.fromSeed`, and ARB-based German localization. Phase 2 builds directly on these foundations. + +**Primary recommendation:** Define three Drift tables (Rooms, Tasks, TaskCompletions), create focused DAOs, expose them through Riverpod stream providers, and implement scheduling logic as a pure Dart utility with comprehensive unit tests. Use `flutter_reorderable_grid_view` for the room card drag-and-drop grid. Store templates as Dart constants (not JSON assets) for type safety and simplicity. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **Room cards & layout**: 2-column grid layout on the Rooms screen. Each card shows room icon, room name, count of due/overdue tasks, thin cleanliness progress bar. No next-task preview or total task count on cards. Cleanliness indicator is a thin horizontal progress bar at bottom of card, fill color shifts green to yellow to red based on ratio of on-time to overdue tasks. Icon picker is a curated grid of ~20-30 hand-picked household Material Icons in a bottom sheet. Cards support drag-and-drop reorder (ROOM-04). Delete room with confirmation dialog that warns about cascade deletion of all tasks (ROOM-03). +- **Task completion & overdue**: Leading checkbox on each task row to mark done -- tap to toggle. No swipe gesture. Tapping the task row (not the checkbox) opens task detail/edit. Overdue visual: due date text turns warm red/coral color. Rest of row stays normal. No undo on completion -- immediate and final. Records timestamp, auto-calculates next due date. Task row info: task name, relative due date (e.g. "Heute", "in 3 Tagen", "Uberfaellig"), and frequency label (e.g. "Woechentlich", "Alle 3 Tage"). No effort indicator or description preview on list view. Tasks within a room sorted by due date (default sort order, TASK-06). +- **Template selection flow**: Post-creation prompt: user creates a room first (name + icon), then gets prompted "Aufgaben aus Vorlagen hinzufuegen?" with template selection. Room type is optional -- used only to determine which templates to suggest. Not stored as a permanent field. If no matching room type is detected, no template prompt appears. All templates unchecked by default -- user explicitly checks what they want. No pre-selection. Users can create fully custom rooms (name + icon only) with no template prompt if no room type matches. Templates cover all 14 room types from TMPL-02. Templates are bundled in the app as static data (German language). +- **Scheduling & recurrence**: Two interval categories: day-count intervals (daily, every N days, weekly, biweekly) add N days from due date, pure arithmetic. Calendar-anchored intervals (monthly, quarterly, every N months, yearly) anchor to original day-of-month with clamping to last day of month but remembering the anchor. Next due calculated from original due date, not completion date. Catch-up on very late completion: keep adding intervals until next due is today or in the future. Custom intervals: user picks a number + unit (Tage/Wochen/Monate). Preset intervals from TASK-04. All due dates stored as date-only (calendar day). + +### Claude's Discretion +- Room creation form layout (full screen vs bottom sheet vs dialog) +- Task creation/edit form layout and field ordering +- Exact Material Icons chosen for the curated icon picker set +- Drag-and-drop reorder implementation approach (ReorderableListView vs custom) +- Delete confirmation dialog design +- Animation on task completion (checkbox fill, row transition) +- Template data structure and storage format (Dart constants vs JSON asset) +- Exact color values for overdue red/coral (within the sage & stone palette) +- Empty state design for rooms with no tasks (following Phase 1 playful tone) + +### Deferred Ideas (OUT OF SCOPE) +None -- discussion stayed within phase scope + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| ROOM-01 | Create a room with name and icon from curated Material Icons set | Drift Rooms table, DAO insert, icon picker bottom sheet with curated icons | +| ROOM-02 | Edit a room's name and icon | DAO update method, room edit form reusing creation form | +| ROOM-03 | Delete a room with confirmation (cascades to associated tasks) | DAO delete with transaction for cascade, confirmation dialog pattern | +| ROOM-04 | Reorder rooms via drag-and-drop on rooms screen | `flutter_reorderable_grid_view` package, `sortOrder` column in Rooms table | +| ROOM-05 | View all rooms as cards showing name, icon, due task count, cleanliness indicator | Drift stream query joining Rooms with Tasks for computed fields, 2-column GridView | +| TASK-01 | Create a task within a room with name, description, frequency interval, effort level | Drift Tasks table, DAO insert, task creation form, frequency/effort enums | +| TASK-02 | Edit a task's name, description, frequency interval, effort level | DAO update method, task edit form reusing creation form | +| TASK-03 | Delete a task with confirmation | DAO delete, confirmation dialog | +| TASK-04 | Set frequency interval from preset list or custom (every N days) | `FrequencyInterval` enum/model, custom interval UI with number + unit picker | +| TASK-05 | Set effort level (low/medium/high) on a task | `EffortLevel` enum stored via `intEnum` in Drift | +| TASK-06 | Sort tasks within a room by due date (default) | Drift `orderBy` on `nextDueDate` column in DAO query | +| TASK-07 | Mark task done via tap, records completion, auto-calculates next due date | TaskCompletions table, scheduling utility, DAO transaction for insert completion + update task | +| TASK-08 | Overdue tasks visually highlighted with distinct color on room cards and task lists | Drift query comparing `nextDueDate` with today, coral/warm red color in theme | +| TMPL-01 | Select from bundled German-language task templates when creating a room | Dart constant maps of room type to template list, template selection bottom sheet | +| TMPL-02 | Preset room types with templates for 14 room types | Static template data covering all 14 room types in German | + + +## Standard Stack + +### Core (already in project) +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| drift | 2.31.0 | Type-safe SQLite ORM | Already established in Phase 1, provides table definitions, DAOs, stream queries, migrations | +| drift_dev | 2.31.0 | Drift code generation | Generates table companions, database classes | +| flutter_riverpod | 3.3.1 | State management | Already established, `@riverpod` code generation with `Ref` | +| riverpod_generator | 4.0.3 | Provider code generation | Generates providers from `@riverpod` annotations | +| go_router | 17.1.0 | Declarative routing | Already established with `StatefulShellRoute`, add nested routes for rooms | +| build_runner | 2.4.0 | Code generation runner | Already in project for drift + riverpod | + +### New Dependencies +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| flutter_reorderable_grid_view | ^5.6.0 | Drag-and-drop reorderable grid | Room cards drag-and-drop reorder (ROOM-04) | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| `flutter_reorderable_grid_view` | `reorderable_grid_view` | `flutter_reorderable_grid_view` is more actively maintained with recent v5.6 overhaul, better animation support | +| `flutter_reorderable_grid_view` | Built-in `ReorderableListView` | Only supports lists, not grids. Room cards need a 2-column grid layout per user decision | +| External date package (`date_kit`) | Custom scheduling utility | Scheduling rules are highly specific (anchor memory, catch-up logic). A custom utility with ~50 lines is simpler and fully testable vs pulling in a dependency for one function | +| JSON asset for templates | Dart constants | Dart constants give type safety, IDE support, and zero parsing overhead. JSON would add asset loading complexity for no benefit | + +### Installation +```bash +flutter pub add flutter_reorderable_grid_view +``` + +No other new dependencies needed. All core libraries are already installed. + +## Architecture Patterns + +### Recommended Project Structure +``` +lib/ + core/ + database/ + database.dart # Add Rooms, Tasks, TaskCompletions tables + database.g.dart # Regenerated with new tables + providers/ + database_provider.dart # Unchanged (existing) + router/ + router.dart # Add nested room routes + theme/ + app_theme.dart # Unchanged (existing) + features/ + rooms/ + data/ + rooms_dao.dart # Room CRUD + stream queries + rooms_dao.g.dart + domain/ + room_icons.dart # Curated Material Icons list + presentation/ + rooms_screen.dart # Replace placeholder with room grid + room_card.dart # Individual room card widget + room_form_screen.dart # Room create/edit form + icon_picker_sheet.dart # Bottom sheet icon picker + room_providers.dart # Riverpod providers for rooms + room_providers.g.dart + tasks/ + data/ + tasks_dao.dart # Task CRUD + stream queries + tasks_dao.g.dart + domain/ + scheduling.dart # Pure Dart scheduling logic + frequency.dart # Frequency interval model + effort_level.dart # Effort level enum + relative_date.dart # German relative date formatter + presentation/ + task_list_screen.dart # Tasks within a room + task_row.dart # Individual task row widget + task_form_screen.dart # Task create/edit form + task_providers.dart # Riverpod providers for tasks + task_providers.g.dart + templates/ + data/ + task_templates.dart # Static Dart constant template data + presentation/ + template_picker_sheet.dart # Template selection bottom sheet + l10n/ + app_de.arb # Add ~40 new localization keys +``` + +### Pattern 1: Drift Table Definition with Enums +**What:** Define tables as Dart classes extending `Table`, use `intEnum()` for enum columns, `references()` for foreign keys. +**When to use:** All database table definitions. +**Example:** +```dart +// Source: drift.simonbinder.eu/dart_api/tables/ +enum EffortLevel { low, medium, high } + +// Frequency is stored as two columns: intervalType (enum) + intervalDays (int) +enum IntervalType { daily, everyNDays, weekly, biweekly, monthly, everyNMonths, quarterly, yearly } + +class Rooms extends Table { + IntColumn get id => integer().autoIncrement()(); + TextColumn get name => text().withLength(min: 1, max: 100)(); + TextColumn get iconName => text()(); // Material Icon name as string + IntColumn get sortOrder => integer().withDefault(const Constant(0))(); + DateTimeColumn get createdAt => dateTime().clientDefault(() => DateTime.now())(); +} + +class Tasks extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get roomId => integer().references(Rooms, #id)(); + TextColumn get name => text().withLength(min: 1, max: 200)(); + TextColumn get description => text().nullable()(); + IntColumn get intervalType => intEnum()(); + IntColumn get intervalDays => integer().withDefault(const Constant(1))(); // For custom intervals + IntColumn get anchorDay => integer().nullable()(); // For calendar-anchored: original day-of-month + IntColumn get effortLevel => intEnum()(); + DateTimeColumn get nextDueDate => dateTime()(); // Date-only, stored as midnight + DateTimeColumn get createdAt => dateTime().clientDefault(() => DateTime.now())(); +} + +class TaskCompletions extends Table { + IntColumn get id => integer().autoIncrement()(); + IntColumn get taskId => integer().references(Tasks, #id)(); + DateTimeColumn get completedAt => dateTime()(); // Timestamp of completion +} +``` + +### Pattern 2: Drift DAO with Stream Queries +**What:** DAOs group related database operations. Stream queries via `.watch()` provide reactive data to the UI. +**When to use:** All data access for rooms and tasks. +**Example:** +```dart +// Source: drift.simonbinder.eu/dart_api/daos/ +@DriftAccessor(tables: [Rooms, Tasks]) +class RoomsDao extends DatabaseAccessor with _$RoomsDaoMixin { + RoomsDao(super.attachedDatabase); + + // Watch all rooms ordered by sortOrder + Stream> watchAllRooms() { + return (select(rooms)..orderBy([(r) => OrderingTerm.asc(r.sortOrder)])).watch(); + } + + // Insert a new room + Future insertRoom(RoomsCompanion room) => into(rooms).insert(room); + + // Update room + Future updateRoom(Room room) => update(rooms).replace(room); + + // Delete room with cascade (transaction) + Future deleteRoom(int roomId) { + return transaction(() async { + // Delete completions for tasks in this room + final taskIds = await (select(tasks)..where((t) => t.roomId.equals(roomId))) + .map((t) => t.id).get(); + for (final taskId in taskIds) { + await (delete(taskCompletions)..where((c) => c.taskId.equals(taskId))).go(); + } + // Delete tasks + await (delete(tasks)..where((t) => t.roomId.equals(roomId))).go(); + // Delete room + await (delete(rooms)..where((r) => r.id.equals(roomId))).go(); + }); + } + + // Reorder rooms + Future reorderRooms(List roomIds) { + return transaction(() async { + for (var i = 0; i < roomIds.length; i++) { + await (update(rooms)..where((r) => r.id.equals(roomIds[i]))) + .write(RoomsCompanion(sortOrder: Value(i))); + } + }); + } +} +``` + +### Pattern 3: Riverpod Stream Provider with Drift +**What:** Wrap Drift `.watch()` streams in `@riverpod` annotated functions that return `Stream`. Riverpod auto-wraps in `AsyncValue` for loading/error/data states. +**When to use:** All reactive data display (room list, task list, room cards with computed stats). +**Example:** +```dart +// Riverpod 3 code generation pattern (matches existing project style) +@riverpod +Stream> roomList(Ref ref) { + final db = ref.watch(appDatabaseProvider); + return db.roomsDao.watchAllRooms(); +} + +// Family provider for tasks in a specific room +@riverpod +Stream> tasksInRoom(Ref ref, int roomId) { + final db = ref.watch(appDatabaseProvider); + return db.tasksDao.watchTasksInRoom(roomId); +} +``` + +### Pattern 4: AsyncNotifier for Mutations +**What:** Class-based `@riverpod` notifier for operations that mutate state (create, update, delete). Follows the existing `ThemeNotifier` pattern. +**When to use:** Room CRUD mutations, task CRUD mutations, task completion. +**Example:** +```dart +@riverpod +class RoomActions extends _$RoomActions { + @override + FutureOr build() {} + + Future createRoom(String name, String iconName) async { + final db = ref.read(appDatabaseProvider); + return db.roomsDao.insertRoom(RoomsCompanion.insert( + name: name, + iconName: iconName, + )); + } + + Future deleteRoom(int roomId) async { + final db = ref.read(appDatabaseProvider); + await db.roomsDao.deleteRoom(roomId); + } +} +``` + +### Pattern 5: Pure Scheduling Utility +**What:** Stateless utility class/functions for calculating next due dates. No database dependency -- pure date arithmetic. +**When to use:** After task completion (TASK-07), during template seeding. +**Example:** +```dart +/// Calculate next due date from the current due date and interval config. +/// For calendar-anchored intervals, [anchorDay] is the original day-of-month. +DateTime calculateNextDueDate({ + required DateTime currentDueDate, + required IntervalType intervalType, + required int intervalDays, + int? anchorDay, +}) { + DateTime next; + switch (intervalType) { + // Day-count: pure arithmetic + case IntervalType.daily: + next = currentDueDate.add(const Duration(days: 1)); + case IntervalType.everyNDays: + next = currentDueDate.add(Duration(days: intervalDays)); + case IntervalType.weekly: + next = currentDueDate.add(const Duration(days: 7)); + case IntervalType.biweekly: + next = currentDueDate.add(const Duration(days: 14)); + // Calendar-anchored: month arithmetic with clamping + case IntervalType.monthly: + next = _addMonths(currentDueDate, 1, anchorDay); + case IntervalType.everyNMonths: + next = _addMonths(currentDueDate, intervalDays, anchorDay); + case IntervalType.quarterly: + next = _addMonths(currentDueDate, 3, anchorDay); + case IntervalType.yearly: + next = _addMonths(currentDueDate, 12, anchorDay); + } + return next; +} + +/// Add months with day-of-month clamping. +/// [anchorDay] remembers the original day for correct clamping. +DateTime _addMonths(DateTime date, int months, int? anchorDay) { + final targetMonth = date.month + months; + final targetYear = date.year + (targetMonth - 1) ~/ 12; + final normalizedMonth = ((targetMonth - 1) % 12) + 1; + final day = anchorDay ?? date.day; + // Last day of target month: day 0 of next month + final lastDay = DateTime(targetYear, normalizedMonth + 1, 0).day; + final clampedDay = day > lastDay ? lastDay : day; + return DateTime(targetYear, normalizedMonth, clampedDay); +} + +/// Catch-up: if next due is in the past, keep adding until future/today. +DateTime catchUpToPresent({ + required DateTime nextDue, + required DateTime today, + required IntervalType intervalType, + required int intervalDays, + int? anchorDay, +}) { + while (nextDue.isBefore(today)) { + nextDue = calculateNextDueDate( + currentDueDate: nextDue, + intervalType: intervalType, + intervalDays: intervalDays, + anchorDay: anchorDay, + ); + } + return nextDue; +} +``` + +### Pattern 6: GoRouter Nested Routes +**What:** Add child routes under the existing `/rooms` branch for room detail and task forms. +**When to use:** Navigation from room grid to room detail, task creation, task editing. +**Example:** +```dart +StatefulShellBranch( + routes: [ + GoRoute( + path: '/rooms', + builder: (context, state) => const RoomsScreen(), + routes: [ + GoRoute( + path: 'new', + builder: (context, state) => const RoomFormScreen(), + ), + GoRoute( + path: ':roomId', + builder: (context, state) { + final roomId = int.parse(state.pathParameters['roomId']!); + return TaskListScreen(roomId: roomId); + }, + routes: [ + GoRoute( + path: 'edit', + builder: (context, state) { + final roomId = int.parse(state.pathParameters['roomId']!); + return RoomFormScreen(roomId: roomId); + }, + ), + GoRoute( + path: 'tasks/new', + builder: (context, state) { + final roomId = int.parse(state.pathParameters['roomId']!); + return TaskFormScreen(roomId: roomId); + }, + ), + GoRoute( + path: 'tasks/:taskId', + builder: (context, state) { + final taskId = int.parse(state.pathParameters['taskId']!); + return TaskFormScreen(taskId: taskId); + }, + ), + ], + ), + ], + ), + ], +), +``` + +### Anti-Patterns to Avoid +- **Putting scheduling logic in the DAO or provider:** Scheduling is pure date math -- keep it in a standalone utility for testability. The DAO should only call the utility, not contain the logic. +- **Using `ref.read` in `build()` for reactive data:** Always use `ref.watch` in widget `build()` methods for Drift stream providers. Use `ref.read` only in callbacks (button presses, etc.). +- **Storing icon as `IconData` or `int` codePoint:** Store the icon name as a `String` (e.g., `"kitchen"`, `"bathtub"`). Map to `IconData` in the presentation layer. This is human-readable and migration-safe. +- **Mixing completion logic with UI:** The "mark done" flow (record completion, calculate next due, update task) should be a single DAO transaction, not scattered across widget callbacks. +- **Using `DateTime.now()` directly in scheduling logic:** Accept `today` as a parameter so tests can use fixed dates. + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Reorderable grid | Custom `GestureDetector` + `AnimatedPositioned` | `flutter_reorderable_grid_view` | Drag-and-drop with auto-scroll, animation, and accessibility is deceptively complex | +| Database reactive streams | Manual `StreamController` + polling | Drift `.watch()` | Drift tracks table changes automatically and re-emits; manual streams miss updates | +| Schema migration | Raw SQL `ALTER TABLE` | Drift `MigrationStrategy` + `Migrator` | Drift validates migrations at compile time with generated helpers | +| AsyncValue loading/error | Manual `isLoading` / `hasError` booleans | Riverpod `AsyncValue.when()` | Pattern matching is exhaustive and handles all states correctly | +| Relative date formatting | Custom if/else chains | Dedicated `formatRelativeDate()` utility | Centralize German labels ("Heute", "Morgen", "in X Tagen", "Ueberfaellig seit X Tagen") in one place for consistency | + +**Key insight:** The main complexity in this phase is the scheduling logic and the Drift schema -- both are areas where custom, well-tested code is appropriate. The UI components (grid reorder, forms, bottom sheets) should lean on existing Flutter/package capabilities. + +## Common Pitfalls + +### Pitfall 1: Drift intEnum Ordering Instability +**What goes wrong:** Adding a new value in the middle of a Dart enum changes the integer indices of all subsequent values, silently corrupting existing database rows. +**Why it happens:** `intEnum` stores the enum's `.index` (0, 1, 2...). Inserting a value shifts all following indices. +**How to avoid:** Always add new enum values at the END. Never reorder or remove enum values. Consider documenting the index mapping in a comment above the enum. +**Warning signs:** Existing tasks suddenly show wrong frequency or effort after a code change. + +### Pitfall 2: Schema Migration from v1 to v2 +**What goes wrong:** Forgetting to increment `schemaVersion` and add migration logic means the app crashes or silently uses the old schema on existing installs. +**Why it happens:** Development uses fresh databases. Real users have v1 databases. +**How to avoid:** Increment `schemaVersion` to 2. Use `MigrationStrategy` with `onUpgrade` that calls `m.createTable()` for each new table. Test migration with `NativeDatabase.memory()` by creating a v1 database, then upgrading. +**Warning signs:** App works on clean install but crashes on existing install. + +### Pitfall 3: Calendar Month Arithmetic Overflow +**What goes wrong:** Adding 1 month to January 31 should give February 28 (or 29), but naive `DateTime(year, month + 1, day)` creates March 3rd because Dart auto-rolls overflow days into the next month. +**Why it happens:** Dart `DateTime` constructor auto-normalizes: `DateTime(2026, 2, 31)` becomes `DateTime(2026, 3, 3)`. +**How to avoid:** Clamp the day to the last day of the target month using `DateTime(year, month + 1, 0).day`. The anchor-day pattern from CONTEXT.md handles this correctly. +**Warning signs:** Monthly tasks due on the 31st drift forward by extra days each month. + +### Pitfall 4: Drift Stream Provider Rebuild Frequency +**What goes wrong:** Drift stream queries fire on ANY write to the table, not just rows matching the query's `where` clause. This can cause excessive rebuilds. +**Why it happens:** Drift's stream invalidation is table-level, not row-level. +**How to avoid:** Keep stream queries focused (filter by room, limit results). Use `ref.select()` in widgets to rebuild only when specific data changes. This is usually not a problem at the scale of a household app but worth knowing. +**Warning signs:** UI jank when completing tasks in one room while viewing another. + +### Pitfall 5: Foreign Key Enforcement +**What goes wrong:** Drift/SQLite does not enforce foreign keys by default. Deleting a room without deleting its tasks leaves orphaned rows. +**Why it happens:** SQLite requires `PRAGMA foreign_keys = ON` to be set per connection. +**How to avoid:** Add `beforeOpen` in `MigrationStrategy` that runs `PRAGMA foreign_keys = ON`. Also implement cascade delete explicitly in the DAO transaction as a safety net. +**Warning signs:** Orphaned tasks with no room after room deletion. + +### Pitfall 6: Reorderable Grid Key Requirement +**What goes wrong:** `flutter_reorderable_grid_view` requires every child to have a unique `Key`. Missing keys cause drag-and-drop to malfunction silently. +**Why it happens:** Flutter's reconciliation algorithm needs keys to track moving widgets. +**How to avoid:** Use `ValueKey(room.id)` on every room card widget. +**Warning signs:** Drag operation doesn't animate correctly, items snap to wrong positions. + +## Code Examples + +### Drift Database Registration with DAOs +```dart +// Source: drift.simonbinder.eu/dart_api/daos/ +@DriftDatabase(tables: [Rooms, Tasks, TaskCompletions], daos: [RoomsDao, TasksDao]) +class AppDatabase extends _$AppDatabase { + AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection()); + + @override + int get schemaVersion => 2; + + @override + MigrationStrategy get migration { + return MigrationStrategy( + onCreate: (Migrator m) async { + await m.createAll(); + }, + onUpgrade: (Migrator m, int from, int to) async { + if (from < 2) { + await m.createTable(rooms); + await m.createTable(tasks); + await m.createTable(taskCompletions); + } + }, + beforeOpen: (details) async { + await customStatement('PRAGMA foreign_keys = ON'); + }, + ); + } + + static QueryExecutor _openConnection() { + return driftDatabase( + name: 'household_keeper', + native: const DriftNativeOptions( + databaseDirectory: getApplicationSupportDirectory, + ), + ); + } +} +``` + +### Task Completion Transaction +```dart +// In TasksDao: mark task done and calculate next due date +Future completeTask(int taskId) { + return transaction(() async { + // 1. Get current task + final task = await (select(tasks)..where((t) => t.id.equals(taskId))).getSingle(); + + // 2. Record completion + await into(taskCompletions).insert(TaskCompletionsCompanion.insert( + taskId: taskId, + completedAt: DateTime.now(), + )); + + // 3. Calculate next due date (from original due date, not today) + var nextDue = calculateNextDueDate( + currentDueDate: task.nextDueDate, + intervalType: task.intervalType, + intervalDays: task.intervalDays, + anchorDay: task.anchorDay, + ); + + // 4. Catch up if next due is still in the past + final today = DateTime.now(); + final todayDateOnly = DateTime(today.year, today.month, today.day); + nextDue = catchUpToPresent( + nextDue: nextDue, + today: todayDateOnly, + intervalType: task.intervalType, + intervalDays: task.intervalDays, + anchorDay: task.anchorDay, + ); + + // 5. Update task with new due date + await (update(tasks)..where((t) => t.id.equals(taskId))) + .write(TasksCompanion(nextDueDate: Value(nextDue))); + }); +} +``` + +### Room Card with Cleanliness Indicator +```dart +// Presentation layer pattern +class RoomCard extends StatelessWidget { + final Room room; + final int dueTaskCount; + final double cleanlinessRatio; // 0.0 (all overdue) to 1.0 (all on-time) + + const RoomCard({ + super.key, + required this.room, + required this.dueTaskCount, + required this.cleanlinessRatio, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + // Green -> Yellow -> Red based on cleanliness ratio + final barColor = Color.lerp( + const Color(0xFFE07A5F), // Warm coral/terracotta (overdue) + const Color(0xFF7A9A6D), // Sage green (clean) + cleanlinessRatio, + )!; + + return Card( + child: InkWell( + onTap: () => context.go('/rooms/${room.id}'), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(_mapIcon(room.iconName), size: 36), + const SizedBox(height: 8), + Text(room.name, style: theme.textTheme.titleSmall), + if (dueTaskCount > 0) + Text('$dueTaskCount faellig', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.error, + )), + const Spacer(), + // Thin cleanliness bar at bottom + LinearProgressIndicator( + value: cleanlinessRatio, + backgroundColor: theme.colorScheme.surfaceContainerHighest, + color: barColor, + minHeight: 3, + ), + ], + ), + ), + ); + } +} +``` + +### Template Data Structure (Dart Constants) +```dart +// Bundled German-language templates as static constants +class TaskTemplate { + final String name; + final String? description; + final IntervalType intervalType; + final int intervalDays; + final EffortLevel effortLevel; + + const TaskTemplate({ + required this.name, + this.description, + required this.intervalType, + this.intervalDays = 1, + required this.effortLevel, + }); +} + +// Room type to template mapping +const Map> roomTemplates = { + 'kueche': [ + TaskTemplate(name: 'Abspuelen', intervalType: IntervalType.daily, effortLevel: EffortLevel.low), + TaskTemplate(name: 'Kuehlschrank reinigen', intervalType: IntervalType.monthly, effortLevel: EffortLevel.medium), + TaskTemplate(name: 'Herd reinigen', intervalType: IntervalType.weekly, effortLevel: EffortLevel.medium), + TaskTemplate(name: 'Muell rausbringen', intervalType: IntervalType.everyNDays, intervalDays: 2, effortLevel: EffortLevel.low), + // ... more templates + ], + 'badezimmer': [ + TaskTemplate(name: 'Toilette putzen', intervalType: IntervalType.weekly, effortLevel: EffortLevel.medium), + TaskTemplate(name: 'Spiegel reinigen', intervalType: IntervalType.weekly, effortLevel: EffortLevel.low), + // ... more templates + ], + // ... all 14 room types +}; + +// Room type detection from name (lightweight matching) +String? detectRoomType(String roomName) { + final lower = roomName.toLowerCase().trim(); + for (final type in roomTemplates.keys) { + if (lower.contains(type) || _aliases[type]?.any((a) => lower.contains(a)) == true) { + return type; + } + } + return null; +} + +const _aliases = { + 'kueche': ['kitchen'], + 'badezimmer': ['bad', 'wc', 'toilette'], + 'schlafzimmer': ['schlafraum'], + // ... more aliases +}; +``` + +### Relative Date Formatter (German) +```dart +/// Format a due date relative to today in German. +/// Source: CONTEXT.md user decision on German labels +String formatRelativeDate(DateTime dueDate, DateTime today) { + final diff = DateTime(dueDate.year, dueDate.month, dueDate.day) + .difference(DateTime(today.year, today.month, today.day)) + .inDays; + + if (diff == 0) return 'Heute'; + if (diff == 1) return 'Morgen'; + if (diff > 1) return 'in $diff Tagen'; + if (diff == -1) return 'Ueberfaellig seit 1 Tag'; + return 'Ueberfaellig seit ${diff.abs()} Tagen'; +} +``` + +### Icon Picker Bottom Sheet +```dart +// Curated household Material Icons (~25 icons) +const List<({String name, IconData icon})> curatedRoomIcons = [ + (name: 'kitchen', icon: Icons.kitchen), + (name: 'bathtub', icon: Icons.bathtub), + (name: 'bed', icon: Icons.bed), + (name: 'living', icon: Icons.living), + (name: 'weekend', icon: Icons.weekend), + (name: 'door_front', icon: Icons.door_front_door), + (name: 'desk', icon: Icons.desk), + (name: 'garage', icon: Icons.garage), + (name: 'balcony', icon: Icons.balcony), + (name: 'local_laundry', icon: Icons.local_laundry_service), + (name: 'stairs', icon: Icons.stairs), + (name: 'child_care', icon: Icons.child_care), + (name: 'single_bed', icon: Icons.single_bed), + (name: 'dining', icon: Icons.dining), + (name: 'yard', icon: Icons.yard), + (name: 'grass', icon: Icons.grass), + (name: 'home', icon: Icons.home), + (name: 'storage', icon: Icons.inventory_2), + (name: 'window', icon: Icons.window), + (name: 'cleaning', icon: Icons.cleaning_services), + (name: 'iron', icon: Icons.iron), + (name: 'microwave', icon: Icons.microwave), + (name: 'shower', icon: Icons.shower), + (name: 'chair', icon: Icons.chair), + (name: 'door_sliding', icon: Icons.door_sliding), +]; +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| `StateNotifier` + manual providers | `@riverpod` code gen + `Notifier`/`AsyncNotifier` | Riverpod 3.0 (Sep 2025) | Simpler syntax, auto-dispose by default, unified `Ref` | +| Manual `StreamController` for DB reactivity | Drift `.watch()` streams | Drift 2.x | Zero-effort reactive queries, auto-invalidation on writes | +| `AutoDisposeStreamProvider` | `@riverpod` returning `Stream` | Riverpod 3.0 | Code generator infers provider type from return type | +| Custom drag-and-drop with `Draggable`+`DragTarget` | `flutter_reorderable_grid_view` 5.x | 2025 v5 rewrite | Performance overhaul, smooth animations, auto-scrolling | + +**Deprecated/outdated:** +- `StateNotifier`/`StateNotifierProvider`: moved to `legacy.dart` import in Riverpod 3.0. Use `Notifier` instead. +- `StateProvider`: also legacy. Use functional `@riverpod` providers. +- Old Riverpod generated ref types (e.g., `AppDatabaseRef`): Riverpod 3 uses plain `Ref`. + +## Open Questions + +1. **Exact number of templates per room type** + - What we know: 14 room types need templates with German names, frequencies, and effort levels + - What's unclear: How many templates per type (3-8 seems reasonable for usability) + - Recommendation: Start with 4-6 templates per room type. Easy to expand later since they're Dart constants. + +2. **Room form as full screen vs bottom sheet** + - What we know: Discretion area. Room creation needs name input + icon picker. + - What's unclear: Whether the flow (name -> icon -> optional templates) fits well in a bottom sheet or needs full screen + - Recommendation: Full-screen form for room creation/edit. It needs a text field, icon picker grid, and potentially template selection. Bottom sheets with keyboard input have known usability issues (keyboard covering content). The template picker that appears after creation can be a separate bottom sheet. + +3. **Task form layout** + - What we know: Discretion area. Tasks need name, optional description, frequency, effort. + - What's unclear: Best field ordering and grouping + - Recommendation: Full-screen form. Fields ordered: name (required, autofocus), frequency (required, segmented button + custom picker), effort (required, 3-option segmented button), description (optional, multiline text). Group required fields at top. + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | flutter_test (built-in) | +| Config file | none -- standard Flutter test setup | +| Quick run command | `flutter test test/features/rooms/ test/features/tasks/ -x` | +| Full suite command | `flutter test` | + +### Phase Requirements to Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| ROOM-01 | Insert room with name + icon via DAO | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 | +| ROOM-02 | Update room name/icon via DAO | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 | +| ROOM-03 | Delete room cascades tasks + completions | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 | +| ROOM-04 | Reorder rooms updates sortOrder | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 | +| ROOM-05 | Watch rooms stream emits with task counts | unit | `flutter test test/features/rooms/data/rooms_dao_test.dart -x` | Wave 0 | +| TASK-01 | Insert task with all fields via DAO | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 | +| TASK-02 | Update task fields via DAO | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 | +| TASK-03 | Delete task with confirmation | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 | +| TASK-04 | All preset intervals produce correct next due dates | unit | `flutter test test/features/tasks/domain/scheduling_test.dart -x` | Wave 0 | +| TASK-05 | Effort level stored and retrieved correctly | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 | +| TASK-06 | Tasks sorted by due date in query | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 | +| TASK-07 | Complete task records completion + updates next due | unit | `flutter test test/features/tasks/data/tasks_dao_test.dart -x` | Wave 0 | +| TASK-08 | Overdue detection based on date comparison | unit | `flutter test test/features/tasks/domain/scheduling_test.dart -x` | Wave 0 | +| TMPL-01 | Template data contains valid entries for each room type | unit | `flutter test test/features/templates/task_templates_test.dart -x` | Wave 0 | +| TMPL-02 | All 14 room types have templates | unit | `flutter test test/features/templates/task_templates_test.dart -x` | Wave 0 | + +### Sampling Rate +- **Per task commit:** `flutter test test/features/rooms/ test/features/tasks/ test/features/templates/` +- **Per wave merge:** `flutter test` +- **Phase gate:** Full suite green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `test/features/rooms/data/rooms_dao_test.dart` -- covers ROOM-01 through ROOM-05 +- [ ] `test/features/tasks/data/tasks_dao_test.dart` -- covers TASK-01 through TASK-03, TASK-05 through TASK-07 +- [ ] `test/features/tasks/domain/scheduling_test.dart` -- covers TASK-04, TASK-07 next due logic, TASK-08 overdue detection, calendar-anchored clamping, catch-up logic +- [ ] `test/features/templates/task_templates_test.dart` -- covers TMPL-01, TMPL-02 (all 14 room types present, valid data) +- [ ] Framework already installed; no additional test dependencies needed. + +## Sources + +### Primary (HIGH confidence) +- [Drift official docs - Tables](https://drift.simonbinder.eu/dart_api/tables/) - Table definition syntax, column types, enums, foreign keys, autoIncrement +- [Drift official docs - DAOs](https://drift.simonbinder.eu/dart_api/daos/) - DAO definition, annotation, CRUD grouping +- [Drift official docs - Writes](https://drift.simonbinder.eu/dart_api/writes/) - Insert with companions, insertReturning, update with where, delete, batch +- [Drift official docs - Select](https://drift.simonbinder.eu/dart_api/select/) - Select with where, watch/stream, orderBy, joins +- [Drift official docs - Streams](https://drift.simonbinder.eu/dart_api/streams/) - Stream query mechanism, update triggers, performance notes +- [Drift official docs - Migrations](https://drift.simonbinder.eu/migrations/) - Schema version, MigrationStrategy, onUpgrade, createTable, PRAGMA foreign_keys +- [Drift official docs - Type converters](https://drift.simonbinder.eu/type_converters/) - intEnum, textEnum, cautionary notes on enum ordering +- [Flutter ReorderableListView API](https://api.flutter.dev/flutter/material/ReorderableListView-class.html) - Built-in reorderable list reference +- Existing project codebase (`database.dart`, `database_provider.dart`, `theme_provider.dart`, `settings_screen.dart`, `rooms_screen.dart`, `router.dart`) - Established patterns for Riverpod 3, Drift, GoRouter + +### Secondary (MEDIUM confidence) +- [Riverpod 3.0 What's New](https://riverpod.dev/docs/whats_new) - Riverpod 3 changes, StreamProvider with code gen, Ref unification +- [flutter_reorderable_grid_view pub.dev](https://pub.dev/packages/flutter_reorderable_grid_view) - v5.6.0, ReorderableBuilder API, ScrollController handling +- [Code with Andrea - AsyncNotifier guide](https://codewithandrea.com/articles/flutter-riverpod-async-notifier/) - AsyncNotifier CRUD patterns +- [Code with Andrea - Riverpod Generator guide](https://codewithandrea.com/articles/flutter-riverpod-generator/) - @riverpod Stream return type generates StreamProvider +- [Dart DateTime API docs](https://api.dart.dev/dart-core/DateTime-class.html) - DateTime constructor auto-normalization behavior + +### Tertiary (LOW confidence) +- [DeepWiki - sample_drift_app state management](https://deepwiki.com/h-enoki/sample_drift_app/5.1-state-management-with-riverpod) - Drift + Riverpod integration patterns (community source, single example) +- [GitHub riverpod issue #3832](https://github.com/rrousselGit/riverpod/issues/3832) - StreamProvider vs StreamBuilder behavior differences (edge case) + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - All libraries already in project except flutter_reorderable_grid_view (well-established package) +- Architecture: HIGH - Patterns directly extend Phase 1 established conventions, verified against existing code +- Pitfalls: HIGH - Drift enum ordering, migration, and PRAGMA foreign_keys are well-documented official concerns +- Scheduling logic: HIGH - Rules are fully specified in CONTEXT.md, Dart DateTime behavior verified against API docs +- Templates: MEDIUM - Template content (exact tasks per room type) needs to be authored, but structure and approach are straightforward + +**Research date:** 2026-03-15 +**Valid until:** 2026-04-15 (stable stack, no fast-moving dependencies)