42 KiB
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>
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 </user_constraints>
<phase_requirements>
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 |
| </phase_requirements> |
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
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<T>() for enum columns, references() for foreign keys.
When to use: All database table definitions.
Example:
// 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<IntervalType>()();
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<EffortLevel>()();
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:
// Source: drift.simonbinder.eu/dart_api/daos/
@DriftAccessor(tables: [Rooms, Tasks])
class RoomsDao extends DatabaseAccessor<AppDatabase> with _$RoomsDaoMixin {
RoomsDao(super.attachedDatabase);
// Watch all rooms ordered by sortOrder
Stream<List<Room>> watchAllRooms() {
return (select(rooms)..orderBy([(r) => OrderingTerm.asc(r.sortOrder)])).watch();
}
// Insert a new room
Future<int> insertRoom(RoomsCompanion room) => into(rooms).insert(room);
// Update room
Future<bool> updateRoom(Room room) => update(rooms).replace(room);
// Delete room with cascade (transaction)
Future<void> 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<void> reorderRooms(List<int> 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<T>. 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:
// Riverpod 3 code generation pattern (matches existing project style)
@riverpod
Stream<List<Room>> roomList(Ref ref) {
final db = ref.watch(appDatabaseProvider);
return db.roomsDao.watchAllRooms();
}
// Family provider for tasks in a specific room
@riverpod
Stream<List<Task>> 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:
@riverpod
class RoomActions extends _$RoomActions {
@override
FutureOr<void> build() {}
Future<int> createRoom(String name, String iconName) async {
final db = ref.read(appDatabaseProvider);
return db.roomsDao.insertRoom(RoomsCompanion.insert(
name: name,
iconName: iconName,
));
}
Future<void> 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:
/// 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:
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.readinbuild()for reactive data: Always useref.watchin widgetbuild()methods for Drift stream providers. Useref.readonly in callbacks (button presses, etc.). - Storing icon as
IconDataorintcodePoint: Store the icon name as aString(e.g.,"kitchen","bathtub"). Map toIconDatain 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: Accepttodayas 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
// 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
// In TasksDao: mark task done and calculate next due date
Future<void> 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
// 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)
// 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<String, List<TaskTemplate>> 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)
/// 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
// 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<T> |
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 tolegacy.dartimport in Riverpod 3.0. UseNotifierinstead.StateProvider: also legacy. Use functional@riverpodproviders.- Old Riverpod generated ref types (e.g.,
AppDatabaseRef): Riverpod 3 uses plainRef.
Open Questions
-
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.
-
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.
-
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-05test/features/tasks/data/tasks_dao_test.dart-- covers TASK-01 through TASK-03, TASK-05 through TASK-07test/features/tasks/domain/scheduling_test.dart-- covers TASK-04, TASK-07 next due logic, TASK-08 overdue detection, calendar-anchored clamping, catch-up logictest/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 - Table definition syntax, column types, enums, foreign keys, autoIncrement
- Drift official docs - DAOs - DAO definition, annotation, CRUD grouping
- Drift official docs - Writes - Insert with companions, insertReturning, update with where, delete, batch
- Drift official docs - Select - Select with where, watch/stream, orderBy, joins
- Drift official docs - Streams - Stream query mechanism, update triggers, performance notes
- Drift official docs - Migrations - Schema version, MigrationStrategy, onUpgrade, createTable, PRAGMA foreign_keys
- Drift official docs - Type converters - intEnum, textEnum, cautionary notes on enum ordering
- Flutter ReorderableListView API - 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 - Riverpod 3 changes, StreamProvider with code gen, Ref unification
- flutter_reorderable_grid_view pub.dev - v5.6.0, ReorderableBuilder API, ScrollController handling
- Code with Andrea - AsyncNotifier guide - AsyncNotifier CRUD patterns
- Code with Andrea - Riverpod Generator guide - @riverpod Stream return type generates StreamProvider
- Dart DateTime API docs - DateTime constructor auto-normalization behavior
Tertiary (LOW confidence)
- DeepWiki - sample_drift_app state management - Drift + Riverpod integration patterns (community source, single example)
- GitHub riverpod issue #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)