# 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)