6.1 KiB
MarianumMobile Client
Flutter-App für die Schul-Community: Stundenplan, Ticker, Newsletter & Co. über das MarianumConnect-Backend, Nextcloud Talk + Files. Zwei Kontoarten: Schul-Konto (Schüler/Lehrer, Benutzername + Passwort) und Eltern-Konto (passwortlos per E-Mail-Code/App-Link, sieht die Stundenpläne der zugeordneten Kinder, kein Talk/Files).
Stack
- Flutter (Dart >= 3.8)
- State:
flutter_bloc+hydrated_bloc(persistente BLoCs pro Modul) - Navigation:
persistent_bottom_nav_bar_v2mit zentralerAppRoutes-Klasse als Single Entry Point - HTTP:
dio, lokales Caching vialocalstore(GenericRequestCache<T>) - Calendar:
syncfusion_flutter_calendar - Datum/Zeit:
jiffy– wird nur über die Extensions inlib/extensions/date_time.dartverwendet - Code-Gen:
freezed,json_serializable
Ordnerstruktur
lib/
├── api/ HTTP-Layer pro Backend (marianumconnect/, marianumcloud/, mhsl/ Legacy, demo/)
├── session/ Session-Modell (CredentialSession / GuardianSession), SessionManager, SessionLifecycle
├── access/ UserRole, AccessRequirement (Gating von Modulen/Settings)
├── auth_link/ Eltern-Login: App-Link-Listener, Link-Parser, Geräte-Bindung
├── state/app/modules/ BLoC pro Feature-Modul (timetable, chat, chat_list, files, ...)
├── state/app/infrastructure LoadableState<T>, DataLoader, geteilte BLoC-Bausteine
├── view/ Screens
│ ├── login/ Login-Flow
│ └── pages/ ein Verzeichnis pro Modul (timetable, files, talk, ...)
├── widget/ Geteilte UI-Komponenten (Dialoge, Buttons, Sheets)
├── extensions/ DateTime-, Text-, TimeOfDay-Extensions
├── routing/ AppRoutes (Single Navigation Entry)
├── theming/ Light/Dark Theme
├── storage/ Freezed Settings-Modelle (HydratedBloc-persistent)
├── notification/ Firebase + flutter_local_notifications
└── utils/ Helper (clipboard_helper, debouncer, download_manager, ...)
Konventionen
Navigation: Ausschließlich über AppRoutes.openX(context, ...). Direkte Navigator.push(...) für volle Pages sind nicht erlaubt – Navigator.pop für Sheets/Dialogs bleibt am Call-Site.
Dialoge:
- Info/Fehler:
InfoDialog.show(context, body, copyable: true, title: '...')auslib/widget/info_dialog.dart. - Bestätigung:
ConfirmDialog(...).asDialog(context)auslib/widget/confirm_dialog.dart. Async-Bestätigung nutztonConfirmAsync(zeigt Spinner und Inline-Fehler überAsyncDialogAction). - Kein inline
AlertDialog/SimpleDialogmehr.
Bottom-Sheets: Detail-Sheets gehen über showDetailsBottomSheet(context, header: ..., children: (ctx) => [...]) aus lib/widget/details_bottom_sheet.dart. Header ist optional.
Async-Actions: Statt manuelles Spinner+Try/Catch die AsyncActionButton-Familie aus lib/widget/async_action_button.dart (AsyncActionButton, AsyncTextButton, AsyncIconButton, AsyncFab, AsyncListTile, AsyncDialogAction, runWithErrorDialog). Fehler-Mapping läuft über errorBuilder oder zentral über errorToUserMessage aus lib/api/errors/error_mapper.dart.
Clipboard: Über copyToClipboard(context, text) aus lib/utils/clipboard_helper.dart. Zeigt automatisch SnackBar.
Datum/Zeit-Formatierung: Über die Extensions in lib/extensions/date_time.dart:
dt.formatHm(), dt.formatDate(), dt.formatDateTime(), dt.formatDateShort(), dt.formatRelative(), start.timeRangeTo(end). Kein direktes Jiffy.parseFromDateTime(...).format(pattern: '...') im View-Code.
Settings: Pro Feature ein Freezed-Modell unter lib/storage/, persistiert via HydratedBloc.
Session: Die aktive Sitzung liegt in SessionManager().current (lib/session/). Nextcloud-Zugriffe nur über SessionManager().requireNextcloud() – Eltern-Sessions haben keine Nextcloud-Identität. Abmelden ausschließlich über SessionLifecycle.signOut(). Die Keychain-Keys in SessionKeys sind eingefroren (Bestandsinstallationen, iOS-NSE).
Rollen & Zuschnitt: Views verzweigen nie auf Rollen. Module und Settings-Sections deklarieren AccessRequirements (AppModule.requirements, Settings._sections); Views bekommen ein Subjekt + eine Policy, die an genau einer Stelle als pure Funktion aufgelöst wird (Vorbild: TimetableSubject + TimetablePolicy.resolve, AbsenceFormPolicy). UserRole nur für Anzeige/Policy-Ableitung.
Stundenplan: Ein TimetableBloc(subject: …) für alle Fälle (eigener Plan, Fremdplan, Kind). Den globalen Bloc stellt PrimaryTimetableScope bereit und tauscht ihn beim Kindwechsel aus; page-scoped Fremdpläne nutzen ScopedTimetableBloc. Die Kinderauswahl (ChildSelectionCubit) ist modulübergreifend.
Build / Run
flutter pub get
dart run build_runner build --delete-conflicting-outputs # nach Änderungen an Freezed/JSON-Modellen
flutter run # Debug auf angeschlossenem Device
flutter analyze # statische Analyse, muss 0 Issues melden
flutter test # Tests (siehe test/)
Backend-Integrationen
| Backend | Pfad | Zweck |
|---|---|---|
| MarianumConnect (Bearer) | lib/api/marianumconnect/ |
Auth, Stundenplan (Webuntis-Proxy), Ticker, Newsletter, Ferien, Abwesenheit, Capabilities, Push |
| Nextcloud (Talk + WebDAV) | lib/api/marianumcloud/ |
Chats, Datei-Verwaltung |
| MHSL (Legacy) | lib/api/mhsl/ |
nur noch Einmal-Migration der Custom Events |
nextcloud-Paket ist auf einen Custom-Fork gepinnt (siehe pubspec.yaml dependency_overrides).
Tests
test/ deckt vor allem pure Funktionen ab (DateTime-Extensions, Stundenplan-Logik, Session-Codec, Policies, Eltern-Login-Controller). Beim Hinzufügen neuer pure-function-Helper bitte Test mit dazu.