Aller au contenu
⬅️ Retour au sommaire ⬅️ Retour au Chapitre 5

✅ Corrigés — Journée 5

Exercice 5.1 — Écran de connexion

Explication de la connexion avec Flutter

et après la connexion :

Explication de la connexion avec Flutter

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).


Exercice 5.2 — Tests

Tests unitaires sur 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é
    });
  });
}

Explication des tests avec Flutter

Test de widget : validation de formulaire vide

// 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

Exercice 5.3 — Générer et installer un APK

É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.


Corrigé du projet final

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

Ce dossier contient :


⬅️ Retour au Chapitre 5 📁 Voir le projet final 🏠 Retour au sommaire