Add the full design specification for the MyIce Flutter mobile app. The app connects to the existing FastAPI backend and replicates the webapp functionality with OIDC auth, schedule browsing, and event detail views.
12 KiB
MyIce Flutter App - Design Specification
Date: 2026-07-06 Context: Application mobile Flutter (iOS/Android) reproduisant le fonctionnement de la webapp myice existante. Backend: L'application se connecte a l'API FastAPI existante (myice/webapi.py) via HTTP.
1. Architecture globale
Approche : Repository + Provider (B)
lib/
├── main.dart # Entry point, MaterialApp, MultiProvider
├── config/
│ └── api_config.dart # URL base de l'API, timeouts
├── models/
│ ├── account.dart # {name, label}
│ ├── event.dart # Game/Practice du schedule
│ ├── event_detail.dart # Details + convocation
│ ├── user_info.dart # {email}
│ └── convocation.dart # Player, Staff, Convocation
├── services/
│ ├── api_service.dart # Dio + intercepteur Bearer
│ └── auth_service.dart # Login/logout OIDC, deep link
├── providers/
│ ├── auth_provider.dart # Etat auth + user info
│ ├── schedule_provider.dart # Liste events + filtres
│ └── event_provider.dart # Details d'un event (optionnel)
├── repositories/
│ └── myice_repository.dart # Appels API : accounts, schedule, game
├── screens/
│ ├── login_screen.dart
│ ├── schedule_screen.dart
│ └── event_detail_screen.dart
└── widgets/
├── event_card.dart
├── filter_dropdown.dart
└── loading_indicator.dart
Pattern : Repository encapsule les appels API -> Providers gerent l'etat reactif -> Screens ecoutent les Providers.
2. Dependances Flutter
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0
provider: ^6.1.0
shared_preferences: ^2.2.0
flutter_web_auth: ^0.6.0
intl: ^0.19.0 # Pour le parsing/formatage de dates si besoin
3. Modeles de donnees (JSON <-> Dart)
Account
class Account {
final String name;
final String label;
Account({required this.name, required this.label});
factory Account.fromJson(Map<String, dynamic> json) => Account(
name: json['name'] as String,
label: json['label'] as String,
);
}
Event
Provenant de /schedule.
class Event {
final String idEvent;
final String agegroup;
final String name;
final String title;
final String opponent;
final String place;
final String start;
final String end;
final String? color;
final String eventType; // mappe depuis le champ JSON 'event'
Event({
required this.idEvent,
required this.agegroup,
required this.name,
required this.title,
required this.opponent,
required this.place,
required this.start,
required this.end,
this.color,
required this.eventType,
});
factory Event.fromJson(Map<String, dynamic> json) => Event(
idEvent: json['id_event']?.toString() ?? '',
agegroup: json['agegroup'] as String,
name: json['name'] as String? ?? '',
title: json['title'] as String,
opponent: json['opponent'] as String? ?? '',
place: json['place'] as String? ?? '',
start: json['start'] as String,
end: json['end'] as String,
color: json['color'] as String?,
eventType: json['event'] as String? ?? '',
);
}
Player
class Player {
final String? position;
final String? number;
final String fname;
final String lname;
final String dob;
Player({this.position, this.number, required this.fname, required this.lname, required this.dob});
factory Player.fromJson(Map<String, dynamic> json) => Player(
position: json['position'] as String?,
number: json['number']?.toString(),
fname: json['fname'] as String,
lname: json['lname'] as String,
dob: json['dob'] as String,
);
}
Staff
class Staff {
final String role;
final String fname;
final String lname;
Staff({required this.role, required this.fname, required this.lname});
factory Staff.fromJson(Map<String, dynamic> json) => Staff(
role: json['role'] as String,
fname: json['fname'] as String,
lname: json['lname'] as String,
);
}
Convocation
class Convocation {
final List<Player> available;
final List<Staff> staff;
Convocation({required this.available, required this.staff});
factory Convocation.fromJson(Map<String, dynamic> json) => Convocation(
available: (json['available'] as List? ?? [])
.map((e) => Player.fromJson(e as Map<String, dynamic>))
.toList(),
staff: (json['staff'] as List? ?? [])
.map((e) => Staff.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
EventDetail
Provenant de /game/{id}.
class EventDetail {
final String title;
final String type;
final String place;
final String timeStart;
final String timeEnd;
final Convocation convocation;
EventDetail({
required this.title,
required this.type,
required this.place,
required this.timeStart,
required this.timeEnd,
required this.convocation,
});
factory EventDetail.fromJson(Map<String, dynamic> json) => EventDetail(
title: json['title'] as String,
type: json['type'] as String,
place: json['place'] as String? ?? '',
timeStart: json['time_start'] as String,
timeEnd: json['time_end'] as String,
convocation: Convocation.fromJson(json['convocation'] as Map<String, dynamic>),
);
}
UserInfo
class UserInfo {
final String email;
UserInfo({required this.email});
factory UserInfo.fromJson(Map<String, dynamic> json) => UserInfo(
email: json['email'] as String? ?? '',
);
}
4. Services
ApiService
Utilise dio. Un intercepteur ajoute automatiquement le header Authorization: Bearer <token>.
Concept:
setBearerToken(token)ajoute unInterceptorsWrappera Dio.clearBearerToken()retire les intercepteurs existants.- Methodes:
getAccounts(),getSchedule(account),getGameDetail(gameId, account),getUserInfo(). - Retournent des objets modeles via
fromJson.
AuthService
Utilise flutter_web_auth pour l'OIDC + shared_preferences pour stocker le token.
Concept:
storedToken,storedAccountdepuisSharedPreferences.login(): appelleFlutterWebAuth.authenticate(url: baseUrl/login, callbackUrlScheme: 'myice'). Parse le fragment#access_token=xxxde l'URL retournee. Stocke le token.setAccount(account): persiste le compte selectionne.logout(): supprime token, account, userInfo des prefs.
Configuration deep link requise:
- Android: intent-filter dans
AndroidManifest.xmlpour schememyice://. - iOS:
CFBundleURLTypesdansInfo.plistavec schememyice.
5. Providers (Provider package)
AuthProvider
Gere l'etat global d'authentification.
Etat: isAuthenticated, userInfo.
Methodes:
init(): Verifie token stocke, appelle/userinfo. Si 401, considere deconnecte.login(): Lance AuthService.login(), met a jour le token, appelle/userinfo, notifie listeners.logout(): Efface le token, notifie listeners.
ScheduleProvider
Gere les events, les comptes, et les filtres.
Etat: accounts, events, selectedAccount, selectedAgegroup, selectedSubgroup, isLoading, error.
Methodes:
loadAccounts(): Appelle/accounts, met a jouraccounts. SelectionnestoredAccountsi present, sinon le premier.loadSchedule(): MetisLoading = true, appelle/schedule?account=selectedAccount, filtre events oueventType == "Jeu", extrait lesagegroupuniques pour les filtres.setAccount(String): Met a jourselectedAccount, sauvegarde dans SharedPreferences, appelleloadSchedule().setAgegroup(String): Met a jour, resetselectedSubgroup, notifie.setSubgroup(String): Met a jour, notifie.get filteredEvents: Getter filtreeventsparagegroupetsubgroup.
Logique de filtre sous-groupe:
Identique a la webapp. La fonction extractSubgroup(name) retire la partie apres " - " dans le name de l'event pour obtenir le sous-groupe (si applicable).
EventDetailProvider (Optionnel)
Gere le chargement des details d'un event. Peut etre integre dans ScheduleProvider si prefere pour garder un seul provider.
Etat: eventDetail, isLoading, error.
Methodes: loadEventDetail(gameId) appelle /game/{gameId}.
6. Ecrans
LoginScreen
- AppBar: Aucune (centre).
- Body: Column centree.
- Titre: "MyIce - Games" (headlineLarge, couleur indigo).
- Bouton: "Se connecter avec Infomaniak" (style degrade indigo -> violet).
- Texte d'aide: "Connectez-vous pour acceder aux evenements...".
- Comportement:
- Si
authProvider.isAuthenticated, navigue automatiquement versScheduleScreen. - Affiche
CircularProgressIndicatorpendant le login. - SnackBar en cas d'erreur.
- Si
ScheduleScreen
- AppBar: Titre "MyIce", action logout + email utilisateur.
- Body:
- Filtres: Card avec 3
DropdownButtonFormField(Compte, Age, Sous-groupe). Le bouton "Charger" est optionnel (changement de compte recharge auto). - Liste:
Expanded+ListView.builder. AfficheEventCardpour chaque event filtre. - Etats: Loading (
CircularProgressIndicator), vide (texte info), erreur (SnackBar).
- Filtres: Card avec 3
EventDetailScreen
- AppBar: Titre =
event.title, bouton retour. - Body:
SingleChildScrollView+Padding(16).- Card recap: Type, Lieu, Heure.
- Section Joueurs:
- Si vide: Card amber "Aucun joueur convoque".
- Sinon: Card avec total + repartition positions. Liste triee par position puis numero:
[Position] #Num - Prenom Nom (DOB).
- Section Personnel:
- Si vide: Card bleu "Aucun personnel convoque".
- Sinon: Liste
role: Prenom Nom.
- Si aucun joueur ET aucun personnel: Card amber principal "Aucun joueur ni personnel convoque" uniquement.
Widgets reutilisables
- EventCard: Card avec bordure laterale coloree. Titre (
agegroup - name), sous-titre, details (adversaire, lieu, heure). onTap ->EventDetailScreen. - FilterDropdown:
DropdownButtonFormFieldgenerique stylise.
7. Navigation & Routing
Utilisation de Navigator classique (pas de package tierce pour 3 ecrans).
main.dart:MaterialAppavechomedetermine parConsumer<AuthProvider>(ScheduleScreenouLoginScreen).- Navigation details:
Navigator.push(MaterialPageRoute(builder: (_) => EventDetailScreen(...))).
8. Gestion des erreurs
- Erreur reseau / API: SnackBar rouge avec message.
- 401 (Token expire): Deconnexion automatique + redirection
LoginScreen. - JSON malforme: La webapp gere cote backend. Cote Flutter,
jsonDecodeechoue -> message "Donnees invalides". - Aucun event/joueur: Interface vide avec message informatif (pas d'erreur).
9. Configuration requise
Android
Ajouter un intent-filter dans android/app/src/main/AndroidManifest.xml pour le deep link myice://callback.
iOS
Ajouter CFBundleURLTypes dans ios/Runner/Info.plist avec scheme myice.
Configuration API
lib/config/api_config.dart:
class ApiConfig {
static const String baseUrl = 'https://your-api-domain.com'; // A configurer
}
10. Plan d'implementation (High-level)
- Scaffolding du projet Flutter (
flutter create). - Ajout des dependances (
pubspec.yaml). - Configuration des deep links (
AndroidManifest+Info.plist). - Implementation des modeles de donnees (
fromJson). - Implementation des services (
ApiService,AuthService). - Implementation des providers (
AuthProvider,ScheduleProvider). - Implementation des ecrans (
LoginScreen,ScheduleScreen,EventDetailScreen). - Implementation des widgets (
EventCard, etc.). - Wiring dans
main.dartavecMultiProvider. - Tests manuels de l'auth OIDC et de la navigation.
Prochaine etape apres validation du spec: Creer le plan d'implementation detaille avec le skill writing-plans.