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

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:44 prefix api/v1).
  • Endpoints (exact, examination.controller.ts / result.controller.ts):
MethodPathNotes
POST/examinationscreate, forced draft
GET/examinations?page&limitpaginated; sort/q ignored (OQ-7)
GET/examinations/:id404 "Examination not found."
PATCH/examinations/:idpartial $set, version+1
DELETE/examinations/:idsoft delete
POST/examinations/:id/subjectsslot create
GET/examinations/:id/subjectsunpaginated slots
POST/examinations/:id/publishstamps publishedAt, status→published
POST/results/exam-subject/:id/marksmarks upsert (EnterMarksDto)
GET/results/exam-subject/:idslot marks (coverage)
GET/results/student/:studentIdown results
GET/results/report-card/:studentId/:examIdaggregate
  • Marks save sends Idempotency-Key: <opId> (offline flush); x-request-id generated per request (00-shared/07 §1).
  • studentId for 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)),
  ]),
)
  • ExamDetailCubit scoped per examId (recreated on route change); MarksEntryCubit scoped per examSubjectId.
  • Cubits expose LoadState-based sealed states (13_State_Management.md); screens are BlocBuilder/BlocListener only — 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 (badge published); dialog is barrierDismissible: false while submitting.
  • Max-marks pre-validation in MarksTextField (digits ≤ maximumMarks, clamp on paste) mirrors the server guard examination.service.ts:145-146; the 404 path refreshes the slot's max before re-entry.
  • Immutability flag exam.status == published gates: 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 in ExamFormCubit before any request.
  • Status picker only offers draft → active transition (OQ-1); create never sends status.
  • Report card: resolve subjectName from Academics subjects catalog by subjectId (payload returns raw id — OQ-9); unmarked rows → "0 + not marked" hint; percentage rendered toStringAsFixed(1) from server's 2-dp value.
  • Names/formatting: times displayed from startTime/endTime strings (never re-parsed); dates from ISO DateTime localized.

7. Build order

  1. Domain entities + enums + repositories (interfaces) — compile-time contract.
  2. Data layer: exams_api.dart (envelope/error decoding), models, repo impls, unit tests with mocked dio.
  3. Cubits + offline queue — pure Dart, unit-tested state machines (13 §7).
  4. Screens S1 → S2 → S3 (core value) with shared widgets (00-shared/03 + 07 module).
  5. S8/S4 forms (08_Form_Specifications.md), S5 publish dialog.
  6. S6/S7 read-only branch (student/parent).
  7. A11y pass (00-shared/09), dark mode, tablet layouts, analytics (proposed) instrumentation per 05.
  8. E2E against live stack (npm run test:e2e — MongoDB + Redis) for 14_QA_Checklist.md scenarios.

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).