Files
HouseHoldKeaper/.planning/phases/02-rooms-and-tasks/02-RESEARCH.md

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

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.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

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

Secondary (MEDIUM confidence)

Tertiary (LOW confidence)

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)