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

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 exactly file (student.controller.ts:83).

5. Key implementation notes

TopicGuidance
Status enumone Dart enum mirroring server; parse with fallback → unknown status renders neutral badge (never crash on new values)
Client-side filtersroster filter chips operate on accumulated items; keep serverPage separate from filteredList (13 §3)
Search honestysearchMode helper text per 09 §1 (server q ignored — OQ-2)
Import CSV parsecsv 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 progressindeterminate; no fake percentage (09 §5, OQ-12)
Document previewgate on file-serving endpoint existence (OQ-9); until then show metadata only
Identity hydrationdetail joins GET /users/:id for name/avatar; fallback initials avatar
Class cascadecache 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
Heroavatar hero tag student-{id} list→detail (00-shared/08 §4)
AnalyticsAnalyticsService events per 14 §11 (proposed)

6. Testing strategy (module)

LayerTests
Unit — cubitsStudentsListCubitTest (state machine incl. filters/loadMore/retry), CreateStudentCubitTest (user-409 → reuse path), ImportCubitTest (report render), TransferCubitTest (blocked status)
Unit — mappersDTO→model incl. unknown status fallback; envelope error mapping
Widgeteach screen 3 states (loading/error/success+empty); roster filter chips; import report card partial success; document upload tile states
Goldenmodule components light/dark × 3 sizes (00-shared/10 §9)
Integrationjourney: 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 with csv, it handles quotes correctly).
  • No chart/markdown/QR deps needed in this module.

8. Perf notes

  • ListView.builder + const tiles; RepaintBoundary on 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.