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

13 — State Management (Results Module)

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

CubitScreenData source
ExamListCubitexam picker (S1/S5 entry)GET /api/v1/examinations (paginated, E8)
SubjectDetailCubitS2GET /api/v1/examinations/:id/subjects (E6)
MarksEntryCubitS1GET /api/v1/results/exam-subject/:id (E2) + POST …/marks (E3)
StudentResultsCubitS3GET /api/v1/results/student/:id (E1) + GET /api/v1/examinations/:id (E5) per group
ReportCardCubitS4GET /api/v1/results/report-card/:studentId/:examId (E4)
GradeSummaryCubitS5E6 + N×E2 (client aggregation) + POST /examinations/:id/publish (E7)

2. Repository layer

  • ResultsRepository — wraps E1–E4; exposes fetchStudentResults, 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-published in-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 ≤ maximumMarks locally, sets dirty, 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-app results-published event.
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 distinct examinationSubjectId → 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; maps subjectName (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); emits Aggregating(progress) per resolved subject → Ready(summary).
  • summary = per-subject coverage + grade distribution (counts of stored grade strings; ungraded bucket) + pass/fail vs passingMarks.
  • publish() → E7; success → re-load + invalidate student result caches; RateLimited(seconds) state on 429.

7. Realtime

  • In-app stream (00-shared/06 §4) consumes results-published (job name per event-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 full EnterMarksDto payload + 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).