Flutter app for NTUT students: course schedules, scores, enrollment, announcements.
Follow @CONTRIBUTING.md for git operation guidelines.
Last updated: 2026-03-03. If stale (>30 days), verify Status section against codebase.
Done:
- PortalService (auth+SSO, getSsoUrl for system browser auth, changePassword, getAvatar, uploadAvatar), CourseService (HTML parsing), ISchoolPlusService (getStudents, getMaterials, getMaterial)
- CalendarService (getCalendar - academic calendar events via calModeApp.do JSON API)
- StudentQueryService (getAcademicPerformance, getRegistrationRecords, getGradeRanking, getStudentProfile)
- HTTP utils, InvalidCookieFilter interceptor
- Drift database schema with all tables
- Service DTOs migrated to Dart 3 records
- Repository stubs (AuthRepository, CourseRepository)
- Riverpod setup (manual providers, no codegen — riverpod_generator incompatible with Drift-generated types)
- Service integration tests (copy
test/test_config.json.exampletotest/test_config.json, then runflutter test --dart-define-from-file=test/test_config.json -r failures-only) - AuthRepository implementation (login, logout, lazy auth via
withAuth<T>(), session persistence via flutter_secure_storage) - go_router navigation setup
- UI: intro screen, login screen, home screen with bottom navigation bar and three tabs (table, score, profile). Uses
StatefulShellRoutewithAnimatedShellContainerfor tab state preservation and cross-fade transitions. Each tab owns its ownScaffold. - i18n (zh_TW, en_US) via slang
Todo - Service Layer:
- ISchoolPlusService: getCourseAnnouncement, getCourseAnnouncementDetail, courseSubscribe, getCourseSubscribe, getSubscribeNotice
- CourseService: getDepartmentMap, getCourseCategory
- CourseService (English): Parse English Course System (
/course/en/) for English names (courses, teachers, syllabus) - StudentQueryService (sa_003_oauth - 學生查詢專區):
- getGPA (學期及歷年GPA查詢)
- getMidtermWarnings (期中預警查詢)
- getStudentAffairs (獎懲、缺曠課、請假查詢)
- getStudentLoan (就學貸款資料查詢)
- getGeneralEducationDimension (查詢已修讀博雅課程向度)
- getEnglishProficiency (查詢英語畢業門檻登錄資料)
- getExamScores (查詢會考電腦閱卷成績)
- getClassAndMentor (註冊編班與導師查詢)
- updateContactInfo (維護個人聯絡資料)
- getGraduationQualifications (查詢畢業資格審查)
Todo - Repository Layer:
- Implement CourseRepository methods (schedules, materials, rosters, caching)
- StudentRepository stub and implementation (grades, GPA, rankings)
Todo - App:
- UI: course table, course detail, scores
- File downloads (progress tracking, notifications, cancellation)
MVVM pattern with Riverpod for DI and reactive state:
- UI calls repository actions directly via constructor providers (
ref.read) - UI observes data through screen-level FutureProviders (
ref.watch) - Repositories encapsulate business logic, coordinate Services (HTTP) and Database (Drift)
Credentials: tool/credentials.dart manages encrypted credentials from the tattoo-credentials Git repo. Run dart run tool/credentials.dart fetch to decrypt and place Firebase configs, Android keystore, and service account. Compatible with match_keystore encryption format (AES-256-CBC, PBKDF2). Config from env vars or .env file.
Structure:
tool/- Dart CLI tools (credentials management)lib/models/- Shared domain enums (DayOfWeek, Period, CourseType, ScoreStatus)lib/repositories/- Repository class + constructor provider (DI wiring)lib/services/- Clients that talk to external systems (NTUT HTTP services, Firebase, etc.)lib/database/- Drift schema and database classlib/utils/- HTTP utilities (cookie jar, interceptors)lib/i18n/- slang i18n YAML sources and generated stringslib/components/- Reusable UI widgets (AppSkeleton, notices, OptionEntryTile, SectionHeader)lib/router/- go_router config and AnimatedShellContainer for tab transitionslib/screens/- Screen widgets organized by feature (welcome/, main/)
Provider placement:
- Constructor providers (DI wiring) are co-located with the classes they construct (services, database, repositories)
- Screen-specific providers live alongside the screen that consumes them (e.g.,
screens/main/profile/profile_providers.dart) - Shared providers used by multiple screens in a feature live one level up
- Repository classes take framework-agnostic dependencies (callbacks, not Riverpod notifiers)
Data Flow Pattern (per Flutter's architecture guide):
- Services return DTOs as records (denormalized, as-parsed from HTML)
- Repositories transform DTOs → normalized DB → return DTOs or domain models
- UI consumes domain models (Drift entities or custom query result classes)
- Repositories handle impedance mismatch between service data and DB structure
Terminology:
- DTOs: Dart records defined in service files - lightweight data transfer objects
- Domain models: Drift entities, Drift view data classes, or custom query result classes - what UI consumes
Services:
- PortalService - Portal auth, SSO
- CalendarService - 學校行事曆 (calModeApp.do JSON API, requires portal session)
- CourseService - 課程系統 (
aa_0010-oauth) - ISchoolPlusService - 北科i學園PLUS (
ischool_plus_oauth) - StudentQueryService - 學生查詢專區 (
sa_003_oauth) - FirebaseService - Unified wrapper for Firebase Analytics and Crashlytics with global
useFirebasetoggle - NTUT services share single cookie jar (NTUT session state)
- NTUT services return DTOs as records (UserDto, SemesterDto, ScheduleDto, etc.) - no database writes
- DTOs are typedef'd records co-located with service implementation
Repositories:
- AuthRepository - User identity, session, profile
- CourseRepository - Course schedules, catalog, materials, rosters, announcements
- StudentRepository (TODO) - Grades, GPA, rankings, warnings, graduation status
- Transform DTOs into relational DB tables
- Return DTOs or domain models to UI
- Handle data persistence and caching strategies
- Method pattern (AuthRepository):
getX({refresh})methods usefetchWithTtlhelper for smart caching - returns cached data if fresh (within TTL), fetches from network if stale. Setrefresh: trueto bypass TTL (pull-to-refresh). Internal_fetchXFromNetwork()methods handle network fetch logic. Special cases that only need partial data (e.g.,getAvatar()only needsavatarFilename) query DB directly. Follow this pattern when implementing other repositories.
Indexing Strategy:
- Avoid premature optimization - this is a personal data app with small datasets (~60-70 courses per student)
- Current indexes are minimal and focused on existing query patterns
- When to add new indexes: When implementing a new feature that introduces SQL queries filtering/joining on non-indexed columns
- Junction table indexes: Composite PKs only support left-to-right lookups. Add separate index if querying by second column alone
- Example:
CourseOfferingStudentsPK{courseOffering, student}supports "students in course" but NOT "courses for student" - Add
course_offering_student_studentindex when implementing student transcript/history queries
- Example:
- Naming convention:
table_column(following Drift examples) - Monitor storage/performance before adding more indexes
- Single-user assumption:
UserRegistrationsview omits theusercolumn — add it and updategetActiveRegistrationfilter if multi-user support is introduced
HTML Parsing: NTUT has no REST APIs. Parse HTML responses with html package.
Shared Cookie Jar: Single cookie jar across all clients for simpler implementation.
SSO Flow: PortalService centralizes auth services. The SSO uses OAuth2 authorization code flow: ssoIndex.do returns an auto-submitting form that POSTs to oauth2Server.do, which 302-redirects to the target service's login endpoint with a code parameter (e.g., LoginOAuthCourseCH.jsp?code=...). This code URL is reusable and cookie-independent — any HTTP client (including a system browser) can open it to establish an authenticated session. PortalService.getSsoUrl(apOu) captures this URL by cloning the Dio instance without RedirectInterceptor to intercept the 302 Location header.
User-Agent: PortalService uses app.ntut.edu.tw endpoints designed for the official NTUT iOS app (User-Agent: Direk ios App). This bypasses login captcha that the web portal (nportal.ntut.edu.tw) requires. Without the correct User-Agent, the server will refuse requests. Browser-based testing of these endpoints won't work.
InvalidCookieFilter: iSchool+ returns malformed cookies; custom interceptor filters them.
Connection: close: PortalService uses Connection: close header. NTUT portal servers close keep-alive connections after multipart uploads, causing stale socket errors if Dart's HTTP client tries to reuse them.
All available SSO service codes are in to doc/ntut_sso_codes.md.
These apOu codes are the SSO target identifiers used by PortalService to obtain service-specific entry URLs/tickets for each NTUT subsystem.