15 — Flutter Implementation Guide (Attendance Module)
- 1. Module folder structure
- 2. Grid performance for 60+ rows
- 3. Offline marking queue
- 4. Repository & networking
- 5. Router
- 6. Widget → cubit wiring (grid page)
- 7. Testing (module additions to
00-shared/11 §12) - 8. Localization & analytics
- 9. Deps delta (none new)
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 date → DateTime (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).
ListView.builder+ fixeditemExtent(56 dp) — no variable-height rows; chips are one line.itemExtentskips layout measurement → cheapest scroll path.constconstructors for row chrome; only the chip + trailing depend on state.- Split widgets, don't rebuild rows:
StatusChipis its ownBlocSelector<MarkingCubit, StatusState, StatusChipState>— a status flip rebuilds one chip, not the list. Row body (name/roll/avatar) isconst-built and never depends on mark state → excluded from rebuild viaSelector. RepaintBoundaryper row — isolates chip animations (pulse, morph) from list painting; prevents repaint storms during rapid taps.- Grid header & MarkedCountBar as
SliverPersistentHeader— sticky without re-layout of rows; counts updated viaBlocBuilderon a counts-only state slice (storeMap<Status,int>precomputed in cubit — never compute inbuild). - Selection model outside rows: selection lives in the cubit (
Set<studentId>); rows render a checkbox overlay only whenselection.isNotEmpty— toggling selection mode rebuilds the batch bar once, not 60 rows (gate withSelectoron mode flag). - Icons: status icons drawn via
Iconwith a cachedIconDatamap (no per-frame asset lookups); chips useAnimatedContainer(cheap) notAnimatedSwitcherinside lists (widget-tree churn); morph toAnimatedSwitcheronly in the popover. - Date keys: every row keyed by
studentId(stable identity forSelectors). - No
setStatein grid page — all state via cubit; page builds only scaffold chrome (layout, filters, batch bar). - Benchmark hook: golden +
WidgetTestertimed 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
ConnectivityCubitoffline; 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: opIdheader 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 bystudentId+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 daysatt.days:{classId}:{date}5 min. - WS: subscribe
attendance.changedfor currentclassId+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 (mirrorattendance.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-228scenarios against live backend.
8. Localization & analytics
.arbkeys:att.status.*,att.grid.*(marked/unmarked counts, sweep, overwrite banner, conflict, offline queued, sync done, device statuses). Dates viaIntltenant locale; wire format alwaysYYYY-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.