15 — Flutter Implementation Guide (Students Module)
- 1. Feature folder structure
- 2. Routing (go_router)
- 3. DI (get_it)
- 4. Repository contracts (dio)
- 5. Key implementation notes
- 6. Testing strategy (module)
- 7. Dependencies to add (module-scoped, verify licenses)
- 8. Perf notes
Build guidance for the Students feature slice on top of 00-shared/11 (project layout, DI, dio, router, theme, testing). Forward-looking spec — no client repo exists yet.
1. Feature folder structure
lib/features/students/
├── data/
│ ├── dto/student_dto.dart # envelope-payload mappers
│ ├── dto/class_enrollment_dto.dart
│ ├── dto/student_document_dto.dart
│ ├── dto/parent_link_dto.dart
│ ├── dto/import_report_dto.dart
│ ├── models/student.dart # domain models (13 §1)
│ ├── models/enrollment.dart
│ ├── models/student_document.dart
│ ├── models/parent_link.dart
│ ├── models/import_report.dart
│ └── repositories/
│ ├── students_repository.dart # list/detail/create/update/delete/enroll/transfer/lifecycle
│ ├── documents_repository.dart
│ ├── parent_links_repository.dart
│ └── bulk_repository.dart # import/export csv
├── domain/
│ └── (skip — 1:1 repo calls; add use cases when >1 repo interaction, e.g. create wizard: users+students)
└── presentation/
├── cubit/students_list_cubit.dart
├── cubit/student_detail_cubit.dart
├── cubit/create_student_cubit.dart
├── cubit/enroll_cubit.dart
├── cubit/transfer_cubit.dart
├── cubit/documents_cubit.dart
├── cubit/history_cubit.dart
├── cubit/parent_links_cubit.dart
├── cubit/import_cubit.dart
├── pages/students_list_page.dart
├── pages/student_detail_page.dart
├── pages/create_student_page.dart
├── pages/import_students_page.dart
├── pages/transfer_page.dart
└── widgets/ # StudentListTile, StatusBadge, StudentHeader,
# AcademicHistoryTimeline, ImportReportCard, RelationshipChip,
# GuardianLinkCard, ClassSelectCascade, DocumentUploadTile,
# RosterFilterBar (07 Component Library)
2. Routing (go_router)
GoRoute(
path: '/students',
parentNavigatorKey: shellNavigatorKey, // StatefulShellBranch
builder: (_, __) => const StudentsListPage(),
routes: [
GoRoute(path: 'add', builder: (_, __) => const CreateStudentPage()),
GoRoute(path: 'import', builder: (_, __) => const ImportStudentsPage()),
GoRoute(
path: ':id',
builder: (_, s) => StudentDetailPage(studentId: s.pathParameters['id']!),
routes: [
GoRoute(path: 'edit', ...),
GoRoute(path: 'transfer', ...),
],
),
],
)
Guards: permissionGuard('student.read') on /students; student.create on
add; student.update on edit/transfer; file.upload on documents actions
(00-shared/11 §6). Deep link studylyon://students/:id → detail.
3. DI (get_it)
getIt.registerLazySingleton<StudentsRepository>(() => StudentsRepository(getIt<AppDio>()));
getIt.registerLazySingleton<DocumentsRepository>(...);
getIt.registerLazySingleton<ParentLinksRepository>(...);
getIt.registerLazySingleton<BulkRepository>(...);
getIt.registerFactory<StudentsListCubit>(() => StudentsListCubit(getIt<StudentsRepository>()));
// … one factory per cubit; no singletons holding screen state (00-shared/11 §2)
Create-wizard dependency on UsersRepository via getIt<UsersRepository>()
(module boundary: repository call, not service — preserves module boundaries in
the client as server events do server-side).
4. Repository contracts (dio)
class StudentsRepository {
Future<Paginated<Student>> list({int page = 1, int limit = 20});
Future<Student> byId(String id);
Future<Student> create(CreateStudentDto dto); // POST /students
Future<Student> update(String id, UpdateStudentDto dto);
Future<void> delete(String id); // soft
Future<Enrollment> enroll(String id, EnrollStudentDto dto);
Future<Student> transfer(String id, TransferStudentDto dto);
Future<Student> graduate(String id);
Future<Student> archive(String id);
Future<Student> restore(String id);
Future<List<Enrollment>> academicHistory(String id);
Future<List<Enrollment>> activeEnrollments(String id);
}
class DocumentsRepository {
Future<List<StudentDocument>> list(String studentId);
Future<StudentDocument> upload(String studentId, Uint8List bytes,
String fileName, String mimeType, String? category); // FormData field "file"
}
class ParentLinksRepository {
Future<List<ParentLink>> linksByStudent(String studentId);
Future<ParentLink> link(String studentId, LinkParentDto dto);
Future<void> unlink(String linkId);
}
class BulkRepository {
Future<ImportReport> importCsv(String entity, Uint8List bytes, String fileName);
Future<String> exportCsv(String entity); // raw csv text
}
- Envelope decoding + typed
ApiException(code,status,message,details)via shared interceptor (00-shared/11 §5). - Timeouts: default 15 s; import 120 s; upload 120 s.
- Multipart:
FormData.fromMap({'file': MultipartFile.fromBytes(...)})— field name exactlyfile(student.controller.ts:83).
5. Key implementation notes
| Topic | Guidance |
|---|---|
| Status enum | one Dart enum mirroring server; parse with fallback → unknown status renders neutral badge (never crash on new values) |
| Client-side filters | roster filter chips operate on accumulated items; keep serverPage separate from filteredList (13 §3) |
| Search honesty | searchMode helper text per 09 §1 (server q ignored — OQ-2) |
| Import CSV parse | csv dart package on an isolate for > 500 rows (00-shared/11 §13); header/column check before upload (mirror students-import.adapter.ts rules client-side for early feedback, server report is authoritative) |
| Import progress | indeterminate; no fake percentage (09 §5, OQ-12) |
| Document preview | gate on file-serving endpoint existence (OQ-9); until then show metadata only |
| Identity hydration | detail joins GET /users/:id for name/avatar; fallback initials avatar |
| Class cascade | cache academics reference lists 24 h (00-shared/06 §3.3); ClassSelectCascade derives class by grade+section+year client-side where by-year list is coarse |
| Hero | avatar hero tag student-{id} list→detail (00-shared/08 §4) |
| Analytics | AnalyticsService events per 14 §11 (proposed) |
6. Testing strategy (module)
| Layer | Tests |
|---|---|
| Unit — cubits | StudentsListCubitTest (state machine incl. filters/loadMore/retry), CreateStudentCubitTest (user-409 → reuse path), ImportCubitTest (report render), TransferCubitTest (blocked status) |
| Unit — mappers | DTO→model incl. unknown status fallback; envelope error mapping |
| Widget | each screen 3 states (loading/error/success+empty); roster filter chips; import report card partial success; document upload tile states |
| Golden | module components light/dark × 3 sizes (00-shared/10 §9) |
| Integration | journey: create user → create student → detail shows ACTIVE → transfer → history shows transferred; import 50-row CSV happy+failed mix |
| E2E (device cloud) | P0: roster → create → documents upload → link parent (00-shared/10 §9) |
7. Dependencies to add (module-scoped, verify licenses)
csv(parse client previews) — or hand-roll minimal splitter for header check (YAGNI: start withcsv, it handles quotes correctly).- No chart/markdown/QR deps needed in this module.
8. Perf notes
ListView.builder+consttiles;RepaintBoundaryon timeline; avatar images cached/resized (cached_network_image).- Tab keep-alive with lazy first-build (documents/history fetch on first activation only).
- Import parse off main isolate for ≥ 500 rows; UI never blocks.