et après la connexion :
import 'package:flutter/material.dart'; import 'auth_service.dart'; // voir Chapitre 5, section 1.2 class PageConnexion extends StatefulWidget { const PageConnexion({super.key}); @override State<PageConnexion> createState() => _PageConnexionState(); } class _PageConnexionState extends State<PageConnexion> { final _formKey = GlobalKey<FormState>(); final _usernameCtrl = TextEditingController(text: 'admin'); final _passwordCtrl = TextEditingController(); final _authService = AuthService(); bool _chargement = false; String? _erreur; @override void dispose() { _usernameCtrl.dispose(); _passwordCtrl.dispose(); super.dispose(); } Future<void> _seConnecter() async { if (!_formKey.currentState!.validate()) return; setState(() { _chargement = true; _erreur = null; }); try { final succes = await _authService.login(_usernameCtrl.text, _passwordCtrl.text); if (!mounted) return; if (succes) { // pushReplacement : remplace l'écran de login dans la pile pour empêcher // de revenir dessus avec le bouton "retour" une fois connecté. Navigator.pushReplacement( context, MaterialPageRoute(builder: (_) => const PageListeEtudiants()), ); } else { setState(() => _erreur = "Identifiants incorrects"); } } catch (e) { setState(() => _erreur = "Impossible de contacter le serveur"); } finally { if (mounted) setState(() => _chargement = false); } } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Connexion")), body: Padding( padding: const EdgeInsets.all(24), child: Form( key: _formKey, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.school, size: 80, color: Colors.teal), const SizedBox(height: 24), TextFormField( controller: _usernameCtrl, decoration: const InputDecoration(labelText: "Nom d'utilisateur", border: OutlineInputBorder()), validator: (v) => (v == null || v.isEmpty) ? "Champ requis" : null, ), const SizedBox(height: 12), TextFormField( controller: _passwordCtrl, obscureText: true, decoration: const InputDecoration(labelText: "Mot de passe", border: OutlineInputBorder()), validator: (v) => (v == null || v.isEmpty) ? "Champ requis" : null, ), if (_erreur != null) ...[ const SizedBox(height: 12), Text(_erreur!, style: const TextStyle(color: Colors.red)), ], const SizedBox(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _chargement ? null : _seConnecter, child: _chargement ? const SizedBox(height: 20, width: 20, child: CircularProgressIndicator(strokeWidth: 2)) : const Text("Se connecter"), ), ), ], ), ), ), ); } }
PageListeEtudiants référencé ici est l’écran développé au Jour 4 (voir corrigé du TP Jour 4).
PageListeEtudiants
Etudiant
// test/etudiant_test.dart import 'package:flutter_test/flutter_test.dart'; import 'package:mon_app/models/etudiant.dart'; void main() { group('Modèle Etudiant', () { test('fromJson désérialise correctement toutes les propriétés', () { final json = {'id': 5, 'nom': 'Chloé Petit', 'email': 'chloe@ecole.fr', 'promotion': 'L3 Info'}; final etudiant = Etudiant.fromJson(json); expect(etudiant.id, 5); expect(etudiant.nom, 'Chloé Petit'); expect(etudiant.email, 'chloe@ecole.fr'); expect(etudiant.promotion, 'L3 Info'); }); test('toJson exclut le champ id lors de la création (id null)', () { final etudiant = Etudiant(nom: 'Nouvel Étudiant', email: 'new@ecole.fr', promotion: 'M1'); final json = etudiant.toJson(); expect(json.containsKey('id'), isFalse); expect(json['nom'], 'Nouvel Étudiant'); }); test('copyWith ne modifie que les champs spécifiés', () { final original = Etudiant(id: 1, nom: 'Alice', email: 'alice@ecole.fr', promotion: 'L3'); final modifie = original.copyWith(promotion: 'M1'); expect(modifie.nom, 'Alice'); // inchangé expect(modifie.email, 'alice@ecole.fr'); // inchangé expect(modifie.promotion, 'M1'); // modifié }); }); }
// test/formulaire_test.dart import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:provider/provider.dart'; import 'package:mon_app/providers/etudiant_provider.dart'; import 'package:mon_app/screens/page_formulaire_etudiant.dart'; void main() { testWidgets('Le formulaire affiche des erreurs si soumis vide', (WidgetTester tester) async { await tester.pumpWidget( ChangeNotifierProvider( create: (_) => EtudiantProvider(), child: const MaterialApp(home: PageFormulaireEtudiant()), ), ); // On soumet le formulaire sans rien saisir await tester.tap(find.text('Enregistrer')); await tester.pump(); // Les messages de validation doivent apparaître expect(find.text('Le nom est requis'), findsOneWidget); expect(find.text('Email invalide'), findsOneWidget); }); }
Lancer tous les tests :
flutter test
Étapes détaillées :
# 1. Se placer dans le projet cd gestion_etudiants # 2. Vérifier que l'URL de l'API pointe vers l'IP locale du PC (pas 10.0.2.2 !) # car l'APK release tournera hors émulateur, sur un vrai réseau Wifi # -> adapter services/etudiant_service.dart : 'http://192.168.1.42:8080/api/etudiants' # 3. Générer l'APK flutter build apk --release # 4. Localiser le fichier généré # build/app/outputs/flutter-apk/app-release.apk # 5. Transférer sur le téléphone (câble, mail, ou serveur local temporaire) python3 -m http.server 8000 --directory build/app/outputs/flutter-apk/ # puis, depuis le navigateur du téléphone (même Wifi) : # http://192.168.1.42:8000/app-release.apk # 6. Sur le téléphone : autoriser "Installer des applications inconnues" # pour l'application utilisée pour ouvrir le fichier (Chrome, Fichiers...)
Vérification finale : débranchez le téléphone du PC, assurez-vous qu’il reste sur le même Wifi que le PC qui héberge le backend Spring Boot (mvn spring-boot:run doit rester lancé), et vérifiez que l’application fonctionne de bout en bout.
mvn spring-boot:run
Même si vous n’avez pas le temps de le terminer, vous pouvez le récupérer et l’utiliser comme ébauche pour votre projet Flutter.
Le corrigé complet du projet final de synthèse (« Gestion Scolaire » : login JWT + CRUD étudiants + recherche + mode sombre + tests) est fourni intégralement en tant que projet Flutter prêt à l’emploi dans le dossier :
📁 projet-final/projet-final.zip
projet-final/projet-final.zip
Ce dossier contient :
pubspec.yaml
lib/models
lib/services
lib/providers
lib/screens
lib/widgets
test/
README.md