Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15 — Flutter Implementation Guide (Attendance Module)

Extends 00-shared/11_Flutter_App_Architecture.md. Focus: grid performance for 60+ rows and the offline marking queue. Forward-looking spec (no client repo exists).


1. Module folder structure

lib/features/attendance/
├── data/
│   ├── dto/attendance_dto.dart          # envelope payload mappers
│   ├── dto/bulk_mark_dto.dart
│   ├── dto/summary_dto.dart             # {total, summary}
│   ├── dto/report_job_dto.dart          # queued/processing/completed/failed
│   ├── models/attendance.dart           # Attendance model (status enum, dates)
│   ├── models/roster_row.dart           # {student, doc?}
│   ├── models/mark_op.dart              # offline queue op
│   └── repositories/
│       ├── attendance_repository.dart   # REST + cache
│       └── offline_mark_queue.dart      # Hive-backed queue
├── domain/
│   └── mark_semantics.dart              # cycle order, conflict detection (pure Dart)
└── presentation/
    ├── cubit/roster_cubit.dart
    ├── cubit/marking_cubit.dart
    ├── cubit/history_cubit.dart
    ├── cubit/report_cubit.dart
    ├── pages/roster_page.dart
    ├── pages/marking_grid_page.dart
    ├── pages/history_page.dart
    ├── pages/report_page.dart
    ├── pages/device_page.dart
    └── widgets/                           # 07_Component_Library.md components

DTO mapping (00-shared/11 §4): server dateDateTime (tenant-local midnight); status → sealed enum matching attendance.schema.ts:7-14; source enum (attendance.schema.ts:16-21) shown only on detail. Never pass DTOs into widgets.

2. Grid performance for 60+ rows

Target: 60 rows, 60 fps, no rebuild storms (00-shared/10 §1, 00-shared/11 §13).

  1. ListView.builder + fixed itemExtent (56 dp) — no variable-height rows; chips are one line. itemExtent skips layout measurement → cheapest scroll path.
  2. const constructors for row chrome; only the chip + trailing depend on state.
  3. Split widgets, don't rebuild rows: StatusChip is its own BlocSelector<MarkingCubit, StatusState, StatusChipState> — a status flip rebuilds one chip, not the list. Row body (name/roll/avatar) is const-built and never depends on mark state → excluded from rebuild via Selector.
  4. RepaintBoundary per row — isolates chip animations (pulse, morph) from list painting; prevents repaint storms during rapid taps.
  5. Grid header & MarkedCountBar as SliverPersistentHeader — sticky without re-layout of rows; counts updated via BlocBuilder on a counts-only state slice (store Map<Status,int> precomputed in cubit — never compute in build).
  6. Selection model outside rows: selection lives in the cubit (Set<studentId>); rows render a checkbox overlay only when selection.isNotEmpty — toggling selection mode rebuilds the batch bar once, not 60 rows (gate with Selector on mode flag).
  7. Icons: status icons drawn via Icon with a cached IconData map (no per-frame asset lookups); chips use AnimatedContainer (cheap) not AnimatedSwitcher inside lists (widget-tree churn); morph to AnimatedSwitcher only in the popover.
  8. Date keys: every row keyed by studentId (stable identity for Selectors).
  9. No setState in grid page — all state via cubit; page builds only scaffold chrome (layout, filters, batch bar).
  10. Benchmark hook: golden + WidgetTester timed build of a 60-row grid (assert < 8 ms row build); profile scroll on mid-range device per release (00-shared/11 §13).

3. Offline marking queue

Persistence: Hive box att.queue keyed {tenantId}:{classId}:{date} (Hive already the chosen cache — 00-shared/11 §11; no new dep).

OfflineMarkQueue
  enqueue(List<MarkOp>)      // dedupe by (studentId,date): replace stale ops
  pending() -> List<MarkOp>  // FIFO
  flush(AppDio)              // -> bulk POST chunks ≤100, Idempotency-Key: opId
  remove(opId); retryBackoff(); conflicted(opId, serverDoc)

Rules (13_State_Management.md §4):

  • Capture on network error or ConnectivityCubit offline; row chip → dashed + cloud icon.
  • Flush on reconnect: sequential ops; per-op success removes; failure keeps with backoff (max 5 attempts → "N marks waiting — review" UI).
  • Idempotency-Key: opId header on every flushed POST (server upsert already makes retries converge — header dedupes events, 00-shared/07 §9).
  • 409 on flush → fetch GET /attendance/:id... (doc id from conflict response or by studentId+date) → conflict preview sheet; user overwrite/discard; never silent.
  • Queue survives session expiry: preserved, flushed after re-login.

4. Repository & networking

  • AttendanceRepository: mark, bulkMark, classByDate(classId, date), studentHistory(studentId, start?, end?), summary(classId, start, end), byId(id), update(id, dto) — all return models; errors → ApiException (00-shared/06 §2).
  • Cache: roster rows 24 h; day marks 5 min SWR (00-shared/06 §3.3); month days att.days:{classId}:{date} 5 min.
  • WS: subscribe attendance.changed for current classId+date → invalidate day cache, refresh marks (silent, no grid reset if user has unsaved edits — merge, don't clobber).
  • Timeouts: grid reads 15 s; bulk 30 s (large payloads); report poll 15 s.

5. Router

Routes (04_Information_Architecture.md §4) via go_router; guards permissionGuard('attendance.mark') etc. Deep link studylyon://attendance/:date → S7 day view (parent alert, (planned) dispatch) — date parsed tenant-local.

6. Widget → cubit wiring (grid page)

MarkingGridPage
 ├─ BlocProvider<MarkingCubit> (created per class+date; disposed on pop)
 ├─ SliverPersistentHeader (MarkedCountBar + StatusFilterChips)  [BlocBuilder: counts slice]
 ├─ SliverList.builder itemExtent 56
 │    └─ MarkingRow
 │        ├─ const RowBody (name, roll)                [never rebuilds]
 │        └─ BlocSelector<StatusChip> (cycle/popover)  [rebuilds on its own status]
 └─ AnimatedSwitcher (batch bar / UndoBar / FAB)

Grid page itself does not rebuild on row state changes.

7. Testing (module additions to 00-shared/11 §12)

  • Unit: mark_semantics (cycle order, conflict detection), OfflineMarkQueue (dedupe, flush, backoff, idempotency), DTO mappers (date tz), summary math (mirror attendance.service.ts:103-113).
  • Widget: grid 3-state (loading/error/success), optimistic flip + rollback, batch bar, conflict banner, undo.
  • Golden: grid (60 rows), chips all statuses, heatmap month, batch sheet — light/dark × 3 sizes (00-shared/10 §9).
  • Integration: login → timetable → mark attendance flow (00-shared/11 §12); offline: mark 5 → airplane → reconnect → flush → server has 5 docs (upsert check).
  • E2E parity: p1-operations.e2e-spec.ts:200-228 scenarios against live backend.

8. Localization & analytics

  • .arb keys: att.status.*, att.grid.* (marked/unmarked counts, sweep, overwrite banner, conflict, offline queued, sync done, device statuses). Dates via Intl tenant locale; wire format always YYYY-MM-DD.
  • Analytics (proposed) events (00-shared/10 §8): attendance.grid.open, attendance.mark.status (studentId excluded — PII guard), attendance.batch.apply, attendance.offline.queue|flush, attendance.report.view, attendance.device.view.

9. Deps delta (none new)

Uses existing stack: flutter_bloc, dio, go_router, get_it, hive, connectivity_plus, intl, fl_chart (donut), secure_storage. No new packages.