13 — State Management (Results Module)
- 1. Cubit map
- 2. Repository layer
- 3. MarksEntryCubit (S1) — the only write state machine
- 4. StudentResultsCubit (S3)
- 5. ReportCardCubit (S4)
- 6. GradeSummaryCubit (S5)
- 7. Realtime
- 8. Offline queue (marks)
Cubits per 00-shared/06_State_Management.md (Bloc; repository layer on top of the v1 API; no server-side state beyond the documents). All reads are cacheable GETs; the only writes are idempotent upserts, so state logic is simple.
1. Cubit map
| Cubit | Screen | Data source |
|---|---|---|
ExamListCubit | exam picker (S1/S5 entry) | GET /api/v1/examinations (paginated, E8) |
SubjectDetailCubit | S2 | GET /api/v1/examinations/:id/subjects (E6) |
MarksEntryCubit | S1 | GET /api/v1/results/exam-subject/:id (E2) + POST …/marks (E3) |
StudentResultsCubit | S3 | GET /api/v1/results/student/:id (E1) + GET /api/v1/examinations/:id (E5) per group |
ReportCardCubit | S4 | GET /api/v1/results/report-card/:studentId/:examId (E4) |
GradeSummaryCubit | S5 | E6 + N×E2 (client aggregation) + POST /examinations/:id/publish (E7) |
2. Repository layer
ResultsRepository— wraps E1–E4; exposesfetchStudentResults,fetchExamSubjectResults,enterMarks,fetchReportCard.ExaminationsRepository— wraps E5–E8.- Cache keys (00-shared/06 §3):
sl:{tenant}:results:student:{studentId},sl:{tenant}:results:subject:{examSubjectId},sl:{tenant}:report-card:{studentId}:{examId},sl:{tenant}:exams:{page}. - Volatile TTL 5 min; report card is the only hard-invalidate candidate —
invalidate on
results-publishedin-app event.
3. MarksEntryCubit (S1) — the only write state machine
sealed class MarksEntryState
├── MarksEntryLoading
├── MarksEntryLoaded(rows, coverage, header) // row = {studentId, marks?, grade?, remarks?, publishedAt?, dirty, saving, error}
├── MarksEntryDirty(rows, pendingCount) // ≥1 row dirty
├── MarksEntrySavingRow(rowId) // per-row upsert in flight
├── MarksEntryRowError(rowId, message, requestId) // failed save, row stays dirty
└── MarksEntryOffline(rows, unsyncedCount) // draft queued locally
load(examSubjectId)→ E2; maps rows; sorts by student name client-side.commitRow(studentId, {marksObtained, grade, remarks})→ validates≤ maximumMarkslocally, setsdirty, fires upsert; on success clears dirty; on 404-over-max keeps dirty +MarksEntryRowError.commitAll()→ replays dirty rows in order (each its own POST, E3).syncOfflineDraft()→ replays the local draft queue (15_Flutter §Offline).setPublished(rowId)→ locks row UI on in-appresults-publishedevent.
stateDiagram-v2
[*] --> Loading
Loading --> Loaded: rows fetched (E2)
Loaded --> Dirty: commitRow (local validation ok)
Dirty --> SavingRow: POST marks (E3)
SavingRow --> Loaded: 2xx (row clean)
SavingRow --> RowError: 404 over-max / network
RowError --> Dirty: user edits again
Dirty --> Offline: connectivity lost
Offline --> SavingRow: syncOfflineDraft replay
Loaded --> Loading: pull-to-refresh
RowError --> Loaded: discarded by user
4. StudentResultsCubit (S3)
S3State: Loading | Loaded(groups) | Empty | Error(code)
load(studentId)→ E1; for each distinctexaminationSubjectId→ E5 (group); E5 404 → orphan row dropped (exam soft-deleted), rest grouped.setFilter(All | Published | Unpublished | grade)— pure client filter, no refetch.
5. ReportCardCubit (S4)
S4State: Loading | Ready(reportCard) | NoSubjects | NotFound | Error(code)
load(studentId, examId)→ E4; mapssubjectName(raw subject ID,result.service.ts:75) → display names via local subject registry.- 404 with message "No subjects found for this examination." (
result.service.ts:51-52) →NoSubjects(guided empty state), other 404s →NotFound.
6. GradeSummaryCubit (S5)
load(examId)→ E6, then N×E2 (parallel, capped concurrency 4); emitsAggregating(progress)per resolved subject →Ready(summary).summary= per-subject coverage + grade distribution (counts of storedgradestrings; ungraded bucket) + pass/fail vspassingMarks.publish()→ E7; success → re-load+ invalidate student result caches;RateLimited(seconds)state on 429.
7. Realtime
- In-app stream (
00-shared/06 §4) consumesresults-published(job name perevent-queue-map.ts:27): MarksEntryCubit locks published rows, ReportCardCubit refreshes, GradeSummaryCubit disables publish. - No per-keystroke network; local-only.
8. Offline queue (marks)
- Draft table keyed
(examSubjectId, studentId); rows carry fullEnterMarksDtopayload + monotonic sequence; replay on reconnect in sequence order; on per-row failure keep row + surface error (B8). - Queue is not a queue manager — plain ordered list, replayed by
syncOfflineDraft()(ponytail: no queue package).