15 — Flutter Implementation Guide (Biometric Module)
- 1. Folder structure
- 2. Dependencies
- 3. Cubits
- 4. BiometricRepository (single)
- 5. Navigation
- 6. Theme
- 7. Extensions
- 8. Localization keys
- 9. Storage
- 10. Testing
- 11. Performance
- 12. Proposals flagged to the team
How to build the Biometric feature in the Flutter client on top of 00-shared/11. Forward-looking spec; no client repo exists yet. Scope note: the PRD excludes native mobile apps — web-first (
PRODUCT_REQUIREMENTS_DOCUMENT.md:144) — so this guide builds the admin console (device/logs/verification), not the capture surface. Capture stays on vendor hardware + SDK (FEATURE_ROADMAP.md:56); QR/push check-in is(forward-looking).
1. Folder structure
features/biometric/
├── domain/
│ ├── models/
│ │ ├── device.dart # DeviceStatus, name, deviceId, model, location, config
│ │ ├── punch.dart # studentId, deviceId, timestamp, mode, rawData?, processed?
│ │ ├── sync_state.dart # idle/running/succeeded/failed/dlq + punches + retries
│ │ └── verify_verdict.dart # matched/mismatched/noPunch + punches
│ └── exceptions/biometric_exceptions.dart
├── data/
│ ├── dto/
│ │ ├── register_device_dto.dart
│ │ └── update_device_dto.dart
│ └── repositories/
│ └── biometric_repository.dart
└── presentation/
├── cubit/
│ ├── biometric_hub_cubit.dart
│ ├── device_list_cubit.dart
│ ├── device_register_cubit.dart
│ ├── device_detail_cubit.dart
│ ├── enroll_cubit.dart
│ ├── log_list_cubit.dart
│ └── verify_checkin_cubit.dart
├── pages/
│ ├── biometric_hub_page.dart
│ ├── device_list_page.dart
│ ├── device_register_page.dart
│ ├── device_detail_page.dart
│ ├── enroll_page.dart
│ ├── log_list_page.dart
│ └── verify_checkin_page.dart
└── widgets/
├── device_status_badge.dart
├── sync_status_tile.dart
├── punch_row.dart
├── punch_timeline.dart
├── enroll_step_bar.dart
├── status_summary_card.dart
├── queue_health_chip.dart
└── config_json_viewer.dart
2. Dependencies
flutter_bloc, dio (AppDio with refresh/error interceptors), go_router, get_it,
intl, json_view (raw payload viewer). No biometric SDK in the app — capture is
device-side; the console only reads results. No local template storage, ever.
3. Cubits
All per 13_State_Management.md; pure Dart, unit-testable. BiometricHubCubit is the
only module-wide state (fleet summary); others are screen-scoped. No singleton beyond the
repository.
4. BiometricRepository (single)
class BiometricRepository {
Future<List<Device>> devices(); // planned GET /biometric/devices
Future<Device> createDevice(RegisterDeviceDto dto); // planned POST /biometric/devices
Future<Device> updateDevice(String id, UpdateDeviceDto); // planned PATCH /biometric/devices/:id
Future<SyncResult> syncDevice(String id); // planned POST /biometric/devices/:id/sync
Future<Paginated<Punch>> logs({filters…}); // planned GET /biometric/logs
Future<EnrollResult> enroll(String deviceId, String studentId); // planned
}
Ingest is not called from the app UI (device webhook); a clerk manual-entry form
(proposed) would call it via the same dio stack with biometric.log.create gating.
5. Navigation
- go_router routes per
04:/biometric,/biometric/devices,/biometric/devices/new,/biometric/devices/:id,/biometric/devices/:id/enroll,/biometric/logs,/biometric/verify. - Route guard: any
biometric.*perm for hub;biometric.device.managefor devices subtree;biometric.log.readfor logs/verify (permissions.constants.ts:41-43). - Deep link
/biometric/logs?student=:idfor dispute sharing.
6. Theme
No new tokens (11_Design_System_Mapping.md); state colors mapped to existing M3 roles.
config_json_viewer monospace on both themes.
7. Extensions
DeviceStatus.presentable()→ localized label + color role.DateTime.toRelative()reused from auth package for punch timestamps.Punch.processedColor()→ tertiary/surfaceVariant (07 §10).
8. Localization keys
biometric.hub.*, biometric.device.*, biometric.enroll.*, biometric.log.*,
biometric.verify.* — full list in 08_Form_Specifications.md; server messages mapped
to keys, fallback to message for business 4xx only.
9. Storage
- No biometric data persisted locally. Only non-sensitive caches: last-good device list,
last-good hub status (
00-shared/06 §5). Never cache punches or verdicts.
10. Testing
- Unit: sync state machine (
13 §1); verdict transitions; DTO→model mappers. - Widget: hub degraded/healthy; device list empty/error; enroll capture-fail retry; log filter chips.
- Golden: all module widgets light/dark × 3 sizes (
00-shared/10 §9). - Integration (mock server): register device → sync → punch appears → verify verdict.
- E2E (when backend lands): register → sync → ingest (simulated device) → dispute → PATCH attendance.
- Security smoke: unauthenticated ingest 401; cross-tenant isolation; no template in any payload.
11. Performance
- Log list paginated +
ListView.builder; punch timeline virtualized per day group. - Hub renders from cached summary first, updates on refresh.
- No full-screen rebuild on sync state change —
SyncStatusTilescoped rebuilds.
12. Proposals flagged to the team
- Build order blocked by backend: device CRUD + logs endpoints, then the
biometric-syncworker (OQ-4), then sync-status UI. The hub screen must not ship with a permanently-empty queue card. - Template storage decision (OQ-3) gates enrollment UI; until then keep
EnrollCubitdesigned but unshipped. biometric.syncpermission name vsbiometric.device.manage— settle before RBAC metadata lands on sync endpoints.- Analytics wiring waits
00-sharedAnalyticsService(proposed). - QR/push check-in stays dormant (
forward-looking) — route exists, no UI.