15 — Flutter Implementation Guide (Exams Module)
- 1. Folder structure
- 2. Models (server-exact)
- 3. API client
- 4. State wiring (BlocProvider scope)
- 5. Offline queue (marks)
- 6. Key implementation rules
- 7. Build order
- 8. Packages (all shared, no new deps)
Concrete Flutter blueprint for the Exams module, aligned with 00-shared/11_Flutter_App_Architecture.md (clean architecture: presentation → application → domain → data; Bloc/Cubit state; repository pattern) and 00-shared/06_State_Management.md. All models map 1:1 to the server contracts in 12_API_Mapping.md.
1. Folder structure
lib/
└── features/exams/
├── exams_module.dart # module wire-up (routes, DI, cubits)
├── domain/
│ ├── entities/
│ │ ├── examination.dart # mirrors examination.schema.ts:9-36
│ │ ├── examination_subject.dart # mirrors examination-subject.schema.ts:9-31
│ │ └── examination_result.dart # mirrors examination-result.schema.ts:9-25
│ ├── enums/
│ │ ├── examination_type.dart # midterm|final|unit_test|quarterly|other
│ │ └── examination_status.dart # draft|active|completed|published
│ └── repositories/
│ ├── examinations_repository.dart # abstract
│ ├── examination_subjects_repository.dart
│ ├── examination_results_repository.dart
│ └── report_card_repository.dart
├── data/
│ ├── models/ # fromJson/toJson + envelope parsing
│ │ ├── examination_model.dart
│ │ ├── examination_subject_model.dart
│ │ ├── examination_result_model.dart
│ │ └── report_card_model.dart # report-card aggregate (result.service.ts:8-26)
│ ├── datasources/
│ │ └── exams_api.dart # dio client, /api/v1, auth + envelope
│ └── repositories_impl/
│ ├── examinations_repository_impl.dart
│ ├── examination_subjects_repository_impl.dart
│ ├── examination_results_repository_impl.dart
│ └── report_card_repository_impl.dart
├── application/
│ ├── cubits/
│ │ ├── exams_list_cubit.dart
│ │ ├── exam_detail_cubit.dart
│ │ ├── exam_form_cubit.dart
│ │ ├── marks_entry_cubit.dart
│ │ ├── student_results_cubit.dart
│ │ └── report_card_cubit.dart
│ └── offline/marks_offline_queue.dart # Hive box 'exm.queue' (13 §4)
└── presentation/
├── screens/
│ ├── exams_list_screen.dart # S1
│ ├── exam_detail_screen.dart # S2 (+ publish dialog S5)
│ ├── marks_entry_screen.dart # S3
│ ├── add_subject_slot_screen.dart # S4
│ ├── exam_form_screen.dart # S8/S8a
│ ├── student_results_screen.dart # S6
│ └── report_card_screen.dart # S7
├── widgets/ # 07_Component_Library.md
│ ├── exam_status_badge.dart
│ ├── exam_type_chip.dart
│ ├── exam_card.dart
│ ├── exam_subject_card.dart
│ ├── marks_row.dart
│ ├── marks_summary_bar.dart
│ ├── grade_chip.dart
│ ├── coverage_tile.dart
│ ├── publish_button.dart / publish_dialog.dart
│ ├── slot_header_card.dart
│ ├── result_row_card.dart
│ ├── report_card_table.dart
│ └── grade_badge.dart
└── routes.dart # /exams, /exams/:id, /exams/:id/slots/:id/marks,
# /exams/:id/slots/new, /exams/new, /exams/:id/edit,
# /results, /results/:examId/report-card
2. Models (server-exact)
enum ExaminationStatus { draft, active, completed, published } // examination.schema.ts:28-33
enum ExaminationType { midterm, final, unitTest, quarterly, other } // :15-20
class Examination {
final String id, academicYearId, name;
final ExaminationType type;
final DateTime startDate, endDate; // ISO → local
final ExaminationStatus status;
final String? gradingSchemeId;
final int version; // base.schema.ts:30-31
}
class ExaminationSubject {
final String id, examinationId, subjectId, classId;
final DateTime date;
final String startTime, endTime; // 'HH:mm' strings — never parsed to DateTime
final int maximumMarks, passingMarks;
}
class ExaminationResult {
final String id, studentId, examinationSubjectId;
final int? marksObtained; // absent = unmarked (report card counts 0)
final String? grade, remarks;
final DateTime? publishedAt;
final int version;
}
class ReportCard { ... } // result.service.ts:8-26, 88-97
Envelope parsing via shared ApiResponse<T> (00-shared/07 §2-3); error decoding
uses error.code (never raw server text except 4xx business messages,
00-shared/07 §11).
3. API client
- Base:
https://api.<domain>/api/v1(main.ts:44prefixapi/v1). - Endpoints (exact,
examination.controller.ts/result.controller.ts):
| Method | Path | Notes |
|---|---|---|
| POST | /examinations | create, forced draft |
| GET | /examinations?page&limit | paginated; sort/q ignored (OQ-7) |
| GET | /examinations/:id | 404 "Examination not found." |
| PATCH | /examinations/:id | partial $set, version+1 |
| DELETE | /examinations/:id | soft delete |
| POST | /examinations/:id/subjects | slot create |
| GET | /examinations/:id/subjects | unpaginated slots |
| POST | /examinations/:id/publish | stamps publishedAt, status→published |
| POST | /results/exam-subject/:id/marks | marks upsert (EnterMarksDto) |
| GET | /results/exam-subject/:id | slot marks (coverage) |
| GET | /results/student/:studentId | own results |
| GET | /results/report-card/:studentId/:examId | aggregate |
- Marks save sends
Idempotency-Key: <opId>(offline flush);x-request-idgenerated per request (00-shared/07 §1). studentIdfor S6/S7 comes from the auth/profile claim — never typed.
4. State wiring (BlocProvider scope)
ExamsModule(
child: MultiBlocProvider(providers: [
BlocProvider(create: (_) => ExamsListCubit(examsRepo)),
BlocProvider(create: (_) => ExamDetailCubit(subjectsRepo, resultsRepo, marksRepo)),
BlocProvider(create: (_) => MarksEntryCubit(resultsRepo, marksOfflineQueue)),
BlocProvider(create: (_) => ExamFormCubit(examsRepo, subjectsRepo)),
BlocProvider(create: (_) => StudentResultsCubit(resultsRepo)),
BlocProvider(create: (_) => ReportCardCubit(reportRepo)),
]),
)
ExamDetailCubitscoped perexamId(recreated on route change);MarksEntryCubitscoped perexamSubjectId.- Cubits expose
LoadState-based sealed states (13_State_Management.md); screens areBlocBuilder/BlocListeneronly — no logic in widgets.
5. Offline queue (marks)
class MarksOfflineQueue {
final HiveBox<MarkOp> box = Hive.box('exm.queue'); // key: {tenant}:{examSubjectId}
// enqueue: dedupe by studentId (last-write-wins), FIFO flush on ConnectivityCubit
// flush: POST per op + Idempotency-Key; 5 attempts → "review" state;
// 409 → conflict preview (server vs local doc)
}
Wire ConnectivityCubit (shared) → MarksEntryCubit.flushQueue(); queue survives
session expiry (00-shared/06 §3.6-3.7).
6. Key implementation rules
- Publish is never optimistic — button awaits
POST /examinations/:id/publish, then reads the refreshed detail (badgepublished); dialog isbarrierDismissible: falsewhile submitting. - Max-marks pre-validation in
MarksTextField(digits ≤maximumMarks, clamp on paste) mirrors the server guardexamination.service.ts:145-146; the 404 path refreshes the slot's max before re-entry. - Immutability flag
exam.status == publishedgates: Publish, Edit, Delete, Add-subject, and marks edits (confirm sheet) — client-side contract (OQ-5). - Client-only validations (OQ-2/3/6): slot overlap warning, duplicate-subject
warning,
passingMarks ≤ maximumMarks, window ⊆ exam dates,endTime > startTime,endDate ≥ startDate— enforced inExamFormCubitbefore any request. - Status picker only offers
draft → activetransition (OQ-1); create never sendsstatus. - Report card: resolve
subjectNamefrom Academics subjects catalog bysubjectId(payload returns raw id — OQ-9); unmarked rows → "0 + not marked" hint; percentage renderedtoStringAsFixed(1)from server's 2-dp value. - Names/formatting: times displayed from
startTime/endTimestrings (never re-parsed); dates from ISODateTimelocalized.
7. Build order
- Domain entities + enums + repositories (interfaces) — compile-time contract.
- Data layer:
exams_api.dart(envelope/error decoding), models, repo impls, unit tests with mocked dio. - Cubits + offline queue — pure Dart, unit-tested state machines (
13 §7). - Screens S1 → S2 → S3 (core value) with shared widgets
(
00-shared/03+07module). - S8/S4 forms (
08_Form_Specifications.md), S5 publish dialog. - S6/S7 read-only branch (student/parent).
- A11y pass (
00-shared/09), dark mode, tablet layouts, analytics(proposed)instrumentation per05. - E2E against live stack (
npm run test:e2e— MongoDB + Redis) for14_QA_Checklist.mdscenarios.
8. Packages (all shared, no new deps)
flutter_bloc, dio (+ interceptors for auth/request-id), hive (offline queue),
intl (dates), go_router (routes incl. deep links studylyon://exams/:id
(forward-looking)), share_plus (report-card share (proposed)), existing shared
design system package (00-shared/02/03).