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 (Results Module)

Client implementation notes against the v1 API. Stack per 00-shared/11_Flutter_App_Architecture.md; state per 13_State_Management.md; components per 07_Component_Library.md. (forward-looking) — mobile is out of Phase 1 (see 01_Product_Overview.md).


1. Marks-entry grid performance (S1)

  • Never use stock DataTable for the entry grid: it builds every cell eagerly. Use a virtualised list:
    • ListView.builder with fixed row extent (itemExtent or prototypeItem) so off-screen rows are not built.
    • Keep row height constant; varying remarks text wraps inside a fixed two-line box (ellipsis) — never IntrinsicHeight.
  • Only visible rows own TextEditingControllers — create controllers in itemBuilder and dispose on scroll-out; a 300-student grid otherwise leaks controllers and rebuilds everything per keystroke.
  • Local edit buffer: TextEditingController.text is the only mutable state (ponytail: no per-cell form model). Commit on Enter/onBlur via MarksEntryCubit.commitRow(...) (13 §3); the cubit owns dirty/saving state.
  • Numeric input: FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')) (decimal allowed — IsNumber has no integer constraint, examination-subject.dto.ts:53), FontFeature.tabularFigures(), mono type (11 §3).
  • Client validation mirrors the server rule before any POST: marksObtained > maximumMarks → inline error, no request (examination.service.ts:145-146).
  • Debounce nothing; commit is user-initiated (Enter/blur), so per-row saving state stays sparse. Batch saves (commitAll) fire sequentially, one in flight per row, others queued — no Future.wait on the whole grid (rows must stay independent).

2. Offline marks draft

  • Local store: drift/sqflite table marks_draft(exam_subject_id, student_id, marks_obtained, grade, remarks, seq INTEGER PRIMARY KEY AUTOINCREMENT) — one row per unsynced edit, seq preserves entry order (13 §8).
  • Upsert semantics make replay safe: re-POSTing the same EnterMarksDto is an update, never a duplicate (examination.service.ts:147-175). No dedupe keys needed.
  • Replay: on connectivity restore, drain in seq order via MarksEntryCubit.syncOfflineDraft(); per-row failure → keep the row, surface error, continue (09 B8). Row is deleted from the draft only on 2xx.
  • Before overwriting a draft row, merge by updatedAt from E2 fetch: if the server row is newer than the draft, keep the server value and drop the draft (server wins — last-write-wins on the server, examination.service.ts:152-161).
  • Conflict policy is deliberately naive (no three-way merge): (planned) revisit when multi-device grading lands.

3. Report card rendering & cache (S4)

  • Cache GET /api/v1/results/report-card/:studentId/:examId keyed (tenant, studentId, examId) TTL 5 min (13 §2); invalidate on results-published in-app event so a published exam's card refreshes immediately (10 I4).
  • Render server-computed values verbatimpercentage, overallGrade, generatedAt come from result.service.ts:83-96; the client never recomputes (single source of truth).
  • subjectName is the raw subject ID (result.service.ts:75) — resolve via a local subject registry (name map from GET /api/v1/subjects, (planned)-friendly: graceful fallback shows the ID if the registry misses).
  • 404 with message "No subjects found for this examination." (result.service.ts:51-52) → NoSubjects guided empty state, not an error screen.

4. Grade summary charts (S5)

  • No chart package. R-GradeDistributionChart = Column of Row(children: [label, Expanded(FractionallySizedBox(widthFactor: n/total, child: container)), countText]) — a dozen lines, semantic labels free.
  • Counts come from stored grade strings (client-supplied; R-GradeChip colour mapping per 11 §1); ungraded bucket = rows without grade.
  • Always render the numeric counts as text next to bars (a11y, 06 §S5).

5. Networking & error mapping

CodeUI
400 VALIDATION_ERRORinline field errors
401 UNAUTHENTICATEDforce re-login
403 PERMISSION_DENIEDerror screen (today unreachable — no RBAC on routes)
404 RESOURCE_NOT_FOUNDover-max normalisation OR empty/not-found states (distinguish by route)
429 RATE_LIMITEDbackoff + countdown (S5 publish)
5xxgeneric + requestId in snackbar (≥ 4 s)
  • Timeout: marks POST 15 s; reads 10 s. Retry policy: reads ×2 exponential; writes: no auto-retry — user-invoked or offline-draft replay only.

6. Realtime

  • results-published in-app event (event-queue-map.ts:27) → SocketListener routes to ReportCardCubit.refresh() and MarksEntryCubit.setPublished(rowId) (10 I4). No other realtime surface exists for this module.

7. Suggested file layout

lib/features/results/
├── data/
│   ├── results_repository.dart        # E1-E4 (13 §2)
│   └── examinations_repository.dart   # E5-E8
├── cubits/                            # per 13 §1
│   ├── marks_entry_cubit.dart
│   ├── student_results_cubit.dart
│   ├── report_card_cubit.dart
│   └── grade_summary_cubit.dart
├── screens/                           # S1-S5
│   ├── marks_entry_screen.dart
│   ├── exam_subject_detail_screen.dart
│   ├── student_results_screen.dart
│   ├── report_card_screen.dart
│   └── grade_summary_screen.dart
├── widgets/                           # 07_Component_Library.md
│   ├── data_table.dart                # R-DataTable
│   ├── grade_chip.dart                # R-GradeChip
│   ├── marks_field.dart               # R-MarksField
│   ├── coverage_bar.dart              # R-CoverageBar
│   ├── grade_distribution_chart.dart  # R-GradeDistributionChart
│   └── publish_panel.dart             # R-PublishPanel
└── offline/
    ├── marks_draft_store.dart         # §2
    └── draft_replayer.dart

8. Golden/unit test hooks

  • computeGrade band edges are server-side — unit-test the client display mapping (11 §1) at 90/89.99/80/… against result.service.ts:130-138 (14 §4).
  • MarksEntryCubit states via bloc_test (13 §3 state machine).
  • DraftReplayer: offline queue order + partial-failure continuation.