15 — Flutter Implementation Guide (Academics Module)
- 1. Folder structure
- 2. Dependencies
- 3. Cubits
- 4. Repositories + the tree join
- 5. Extensions / joiners (shared
00-shared/11 §8+ module) - 6. Navigation
- 7. Theme
- 8. Localization keys
- 9. Storage
- 10. Testing
- 11. Performance
- 12. Proposals flagged to the team
How to build the Academics feature in the Flutter client on top of 00-shared/11. Forward-looking spec; no client repo exists yet. All DTO/API facts are exact from
src/modules/academics/**(12_API_Mapping.md).
1. Folder structure
features/academics/
├── domain/
│ ├── models/
│ │ ├── academic_year.dart # id, name, startDate, endDate, status, isCurrent
│ │ ├── grade.dart # id, academicYearId?, name, code?, displayOrder, status
│ │ ├── section.dart # id, gradeId, name, capacity, classTeacherId?, roomId?, status
│ │ ├── school_class.dart # id, academicYearId, campusId?, gradeId, sectionId,
│ │ │ # name, capacity, classTeacherId?, roomId?, status
│ │ ├── subject.dart # id, code, name, shortName?, credits, maximumMarks,
│ │ │ # passingMarks, theoryMarks, practicalMarks, status
│ │ └── subject_assignment.dart # id, teacherId, subjectId, classId, academicYearId
│ └── exceptions/academics_exceptions.dart
├── data/
│ ├── dto/
│ │ ├── create_academic_year_dto.dart … create_subject_dto.dart
│ │ ├── update_*_dto.dart (5 entities)
│ │ └── create_subject_assignment_dto.dart
│ └── repositories/
│ ├── academic_year_repository.dart
│ ├── grade_repository.dart
│ ├── section_repository.dart
│ ├── class_repository.dart
│ ├── subject_repository.dart
│ ├── subject_assignment_repository.dart
│ └── reference_repository.dart # aggregated cache loader
└── presentation/
├── cubit/
│ ├── reference_cubit.dart # §2 (13_State_Management §3)
│ ├── cascading_picker_cubit.dart
│ ├── year_list_cubit.dart, grade_list_cubit.dart, class_list_cubit.dart,
│ │ section_list_cubit.dart, subject_list_cubit.dart
│ ├── class_detail_cubit.dart, assignment_matrix_cubit.dart,
│ │ roster_cubit.dart, explorer_cubit.dart
│ └── year_form_cubit.dart, grade_form_cubit.dart, section_form_cubit.dart,
│ class_form_cubit.dart, subject_form_cubit.dart
├── pages/
│ ├── year_list_page.dart, year_detail_page.dart
│ ├── grade_list_page.dart, grade_detail_page.dart
│ ├── class_list_page.dart, class_detail_page.dart, class_assign_page.dart
│ ├── section_list_page.dart, section_detail_page.dart
│ ├── subject_list_page.dart, subject_detail_page.dart
│ ├── explorer_page.dart, teacher_roster_page.dart
│ └── forms/ (year_form_page, grade_form_page, section_form_page,
│ class_form_page, subject_form_page)
└── widgets/
├── hierarchy_tree.dart, section_chips.dart, subject_assignment_row.dart,
│ year_status_badge.dart, entity_status_badge.dart,
│ cascading_entity_picker.dart, marks_summary_tiles.dart,
│ roster_table.dart, conflict_banner.dart, reference_cache_provider.dart
└── joiners.dart # label resolution (§5)
2. Dependencies
flutter_bloc, dio (AppDio with refresh/error interceptors, 00-shared/11 §5),
go_router, get_it, hive (reference cache persistence) or SharedPreferences +
in-memory index, intl, collection (groupBy). No tree package — HierarchyTree
is a custom AnimatedSize list (07 §1).
3. Cubits
ReferenceCubit: singleton per tenant (recreated on tenant switch,13 §10); state{years, grades, sections, classes, subjects, selectedYearId, ttl}; emitsReferenceChangedon any write/refresh (13 §5).- List cubits mix
PaginationCubit(00-shared/06 §3.2); year-scoped variants holdMap<String, PageState>per year. - Form cubits: single
Submit(dto); map 409 →duplicatestate with server message; 400 → field errors fromerror.detailskeys (DTO names). AssignmentMatrixCubit:Addguards against already-assigned pairs using the loaded roster (client-only guard, OQ-4);Replace=Remove+Addsequence.ExplorerCubit: builds the tree fromReferenceCubitcache; never refetches per node (no tree endpoint).
4. Repositories + the tree join
class ReferenceRepository {
// single-flight per collection; caches pages until TTL (24 h)
Future<List<AcademicYear>> years(); // GET /academic-years (all pages)
Future<List<Grade>> grades(); // GET /grades (all pages)
Future<List<Section>> sections({String? gradeId}); // GET /sections[/by-grade/:id]
Future<List<SchoolClass>> classes({String? academicYearId}); // GET /classes[/by-year/:id]
Future<List<Subject>> subjects(); // GET /subjects (all pages)
Future<List<SubjectAssignment>> rosterByClass(String classId, String yearId); // by-class
Future<List<SubjectAssignment>> rosterByTeacher(String teacherId, String yearId); // by-teacher
}
Hierarchy join (no server tree): build nested nodes from cached lists:
- classes →
schoolClass.gradeId→ grade;schoolClass.sectionId→ section; grades optionally bound tograde.academicYearId(floaters shown in all years,grade.schema.ts:9-10); classes grouped under their year viaclass.academicYearId. - Each class leaf's subject count = roster cache
[classId]length (fetched lazily per expanded class,by-class).
5. Extensions / joiners (shared 00-shared/11 §8 + module)
Grade.section(sections)→ label;SchoolClass.labels(grades, sections, years)→(gradeName, sectionName, yearName)for chips — never guessed server-side.SubjectAssignment.classLabel()/.subjectLabel()resolution; missing ref → "Unknown (deleted)" chip (OQ-3 orphan handling).String.toTitle()for section names;String.upperCode()for subject codes.DateTime.toIsoDate()(submitYYYY-MM-DDonly —IsDateString,create-academic-year.dto.ts:9-15);DateTime.fromIso()for display.YearStatusBadgemappingstatusenum (academic-year.schema.ts:7-11).- Numerals:
FontFeature.tabularFigures()on capacity/marks/orders.
6. Navigation
go_router GoRoutes per 04 §3 (prefix /academics); route guards
authGuard + role check (00-shared/05 §9); academics.* permission guard
(planned) — OQ-1. Year switcher is a shell-level control bound to
ReferenceCubit.selectedYearId, not per-page state. Deep links:
/academics/classes/:id, /academics/explore?yearId= (10 §7).
7. Theme
Global AppTheme unchanged; module adds no tokens. Patterns from
11_Design_System_Mapping.md: conflict tertiaryContainer, current primaryContainer,
status badge maps.
8. Localization keys
academics.*: years.*, grades.*, sections.*, classes.*, subjects.*,
assignments.*, explorer.*, forms.*, conflicts.*. Server business messages
(409 copies) rendered verbatim via a serverMessage key with fallback to the raw
message (00-shared/09 policy in 14 §11).
9. Storage
- Reference cache →
Hiveboxesacademics.years/grades/sections/classes/subjects(non-sensitive,00-shared/11 §11), TTL 24 h (13 §3). - No secure-storage usage in this module (no tokens/secrets).
10. Testing
- Unit: cascade picker transitions; pagination per-year maps; duplicate
pre-checks (year/grade/subject code); marks cross-validation
(
theory+practical ≤ maximum,passing ≤ maximum— OQ-5); DTO → model mappers. - Widget: class form cascade disabled-states; roster conflict banner;
explorer expand ≤ 50 nodes;
SectionChipsoverflow. - Golden:
HierarchyTree,SectionChips,SubjectAssignmentRow,YearStatusBadge,CascadingEntityPickerlight/dark × 3 sizes. - Integration: mock-server flows per
14 §QA scripts. - E2E (P0): fresh tenant full ladder setup → timetable/attendance reflect new class; multi-device set-current correction.
11. Performance
- Cache-first render: screens paint from Hive before network revalidation (no blank spinners on warm start).
HierarchyTree: lazy children, ≤ 50 expanded nodes,ListView.builder(07 §1).- Roster rows cached per class (LRU 20,
13 §1);by-classfetched on class detail open only. constconstructors; no whole-page rebuild on filter chips (scopedBlocBuilders).- Pagination: infinite scroll fetches next page only when
hasNext(12 §9).
12. Proposals flagged to the team
- RBAC first (OQ-1): today any authenticated user can write structure —
ship UI gating + flag backend perms (
academics.read/.create/.update/.deleteper entity) before student-facing read mode goes live. - When server adds duplicate/conflict validation (OQ-4), remove client-only guards and render server 409/422 messages instead.
- When
qsearch lands server-side (OQ-7), swap local filters for query params. - When campus/room modules land (OQ-6), replace hidden
campusIdfield + freeroomIdtext with real pickers. - Coaching extension (
docs/IMPLEMENTATION_PLAN.md:314-317,402-429): sections → batches,subjectCategory/classType/batchId— gate behindFeatureFlagsCubit; models gain optional fields, UI unchanged until enabled. - Analytics wiring waits shared
AnalyticsServiceinterface (00-shared/10 §8).