Aller au contenu
⬅️ Retour au sommaire Flutter ⬅️ Chapitre précédent

Chapitre 5 - Journée 5 : Authentification, tests, déploiement et projet final

Vous voici arrivé à la fin de la formation Flutter. Il faut évidemment pratiquer. Aujourd’hui, nous abordons l’authentification avec JWT, la mise en place des tests et puis le déploiement d’une application mobile. Vous pourrez, soit reprendre un des TP déjà réalisé ou bien le projet final de synthèse. L’objectif étant que vous puissiez développer une application mobile de A à Z en Flutter les jours qui restent…

Sommaire du chapitre

  1. Authentification JWT
  2. Écrire des tests
  3. Build et déploiement
  4. Bonnes pratiques d’architecture
  5. Exercices du jour
  6. Projet final de synthèse

1. Authentification JWT

Vous connaissez déjà JWT côté Spring Boot (Spring Security). Voici un diagramme de séquence basique pour illustrer le côté Flutter, le flux est le suivant :

flutter - spring jwt

1.1 Stockage sécurisé du token

⚠️ Ne stockez jamais un token JWT avec shared_preferences (non chiffré) en production. On utilisera le package flutter_secure_storage qui s’appuie sur le Keychain iOS et le Keystore Android.

Voici ci-dessous, comment procéder :

  1. On ajoute la dépendance
dependencies:
  flutter_secure_storage: ^9.2.2
  1. On importe notre package pour instancier un FlutterSecureStorage

Ainsi, nous allons pouvoir utiliser les méthodes write(), read() et delete() qui fonctionne sur le principe de clef/valeur. Ici, on crée une classe TokenStorage avec les méthodes appropriées.

import 'package:flutter_secure_storage/flutter_secure_storage.dart';

class TokenStorage {
  final _storage = const FlutterSecureStorage();
  static const _cleToken = 'jwt_token';

  Future<void> sauvegarder(String token) => _storage.write(key: _cleToken, value: token);
  Future<String?> lire() => _storage.read(key: _cleToken);
  Future<void> supprimer() => _storage.delete(key: _cleToken);
}

1.2 Service d’authentification

Notre classe AuthService va utiliser notre TokenStorage créé précédemment.

class AuthService {
  static const String _baseUrl = 'http://10.0.2.2:8080/api/auth';
  final TokenStorage _tokenStorage = TokenStorage();

  Future<bool> login(String username, String password) async {
    final response = await http.post(
      Uri.parse('$_baseUrl/login'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({'username': username, 'password': password}),
    );

    if (response.statusCode == 200) {   // ok
      final data = jsonDecode(response.body); // on récupère les données dans le corps
      await _tokenStorage.sauvegarder(data['token']); // on enregistre notre token
      return true;
    }
    return false;
  }
  // que fait-on si on quitte l'application ? Il faut penser à détruire le token !
  Future<void> logout() => _tokenStorage.supprimer();
  Future<bool> estConnecte() async => (await _tokenStorage.lire()) != null;
}

1.3 Ajouter le token à chaque requête protégée

C’est tout simple, il suffit de lire notre token enregistré dans notre TokenStorage etle faire passer avec Bearer.

Future<http.Response> requeteAuthentifiee(Uri url) async {
  final token = await TokenStorage().lire();
  return http.get(
    url,
    headers: {
      'Content-Type': 'application/json',
      if (token != null) 'Authorization': 'Bearer $token',
    },
  );
}

💡 Sur un projet réel avec le package Dio, on utilise un Interceptor pour ajouter automatiquement ce header à toutes les requêtes sortantes. C’est l’équivalent d’un OncePerRequestFilter, une sorte de Filter chain côté Spring Security qui intercepte toutes les requêtes avant qu’elles ne soient traitées !

1.4 Gérer l’expiration du token (401)

Il faut aussi penser à gérer les exceptions.

flutter - spring token avec expiration

final response = await requeteAuthentifiee(url);
if (response.statusCode == 401) {
  await AuthService().logout();
  // on redirige vers l'écran de connexion
}

2. Écrire des tests

Comme JUnit côté Spring Boot, Flutter fournit un framework de test complet, avec 3 niveaux :

Type de test Équivalent Spring Boot Package Flutter
Test unitaire (logique pure) @Test JUnit sur un @Service test
Test de widget Test MockMvc sur un @Controller flutter_test
Test d’intégration (bout en bout) @SpringBootTest avec vrai serveur integration_test

2.1 Test unitaire

// test/etudiant_test.dart
import 'package:flutter_test/flutter_test.dart';
import 'package:mon_app/models/etudiant.dart';

void main() {
  group('Etudiant', () {
    test('fromJson désérialise correctement', () {
      final json = {'id': 1, 'nom': 'Alice', 'email': 'alice@test.fr', 'promotion': 'L3'};
      final etudiant = Etudiant.fromJson(json);

      expect(etudiant.id, 1);
      expect(etudiant.nom, 'Alice');
      expect(etudiant.email, 'alice@test.fr');
    });

    test('toJson ne contient pas id si null', () {
      final etudiant = Etudiant(nom: 'Bob', email: 'bob@test.fr', promotion: 'M1');
      final json = etudiant.toJson();

      expect(json.containsKey('id'), false);
      expect(json['nom'], 'Bob');
    });
  });
}
flutter test

2.2 Test de widget

// test/widget_test.dart
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mon_app/main.dart';

void main() {
  testWidgets('Le compteur s\'incrémente au clic', (WidgetTester tester) async {
    await tester.pumpWidget(const MonApplication());

    // Vérifie l'état initial
    expect(find.text('0'), findsOneWidget);
    expect(find.text('1'), findsNothing);

    // Simule un clic sur le bouton "+"
    await tester.tap(find.byIcon(Icons.add));
    await tester.pump(); // reconstruit le widget après le setState

    // Vérifie le nouvel état
    expect(find.text('0'), findsNothing);
    expect(find.text('1'), findsOneWidget);
  });
}

2.3 Mocker le service réseau (isoler le test de l’API réelle)

dev_dependencies:
  mockito: ^5.4.4
  build_runner: ^2.4.9
// Simplifié : test du provider avec un faux service (sans appel réseau réel)
class FakeEtudiantService implements EtudiantService {
  @override
  Future<List<Etudiant>> getTous() async {
    return [Etudiant(id: 1, nom: 'Test', email: 'test@test.fr', promotion: 'L3')];
  }
  // ... autres méthodes non utilisées dans ce test
}

Exactement comme vous mockeriez un @Repository avec Mockito côté Spring pour tester un @Service en isolation, sans base de données réelle.


3. Build et déploiement

3.1 Android - générer un App Bundle (format requis par le Play Store)

flutter build appbundle --release
# fichier généré : build/app/outputs/bundle/release/app-release.aab

Pour un simple APK partageable manuellement (hors store) :

flutter build apk --release --split-per-abi

Signature de l’application (obligatoire pour publier) : générez une clé, configurez android/key.properties et android/app/build.gradle selon la documentation officielle.

3.2 iOS — générer un build (nécessite un Mac)

flutter build ios --release

Puis, dans Xcode : Product → Archive, puis Distribute App vers TestFlight ou l’App Store. Un compte développeur Apple payant (99 $/an) est nécessaire pour publier sur l’App Store (le compte gratuit ne permet que les tests locaux).

3.3 Sans Mac : builder et tester via le cloud

Des services comme Codemagic permettent de :

3.4 Checklist avant publication


4. Bonnes pratiques d’architecture

Récapitulatif de l’architecture en couches recommandée, celle utilisée dans tous les corrigés de ce cours :

lib/
├── main.dart                  # point d'entrée, configuration des providers
├── models/                    # DTO (Etudiant, Tache, Utilisateur...)
├── services/                  # appels réseau (EtudiantService, AuthService...)
├── providers/                 # état de l'application (ChangeNotifier)
├── screens/                   # écrans complets (une page = un fichier)
└── widgets/                   # composants réutilisables entre écrans

Cette organisation reproduit fidèlement les couches Controller / Service / Repository / Entity de Spring Boot, avec en plus la couche widgets/ propre au mobile (l’équivalent de composants front-end réutilisables).


5. Exercices du jour

Exercice 5.1 — Écran de connexion

Implémentez un écran de login (username/mot de passe) qui appelle AuthService.login() (Annexe A). En cas de succès, naviguez vers l’écran principal en remplaçant la pile de navigation (Navigator.pushReplacement). En cas d’échec, affichez un message d’erreur.

Explication de la connexion avec Flutter

Exercice 5.2 — Tests

Écrivez 3 tests unitaires sur le modèle Etudiant (sérialisation/désérialisation) et 1 test de widget vérifiant qu’un formulaire vide affiche bien les messages de validation.

Pour vous aider :

Explication des tests avec Flutter

Exercice 5.3 — Générer un APK

Générez un APK release de votre application du Jour 4, installez-le manuellement sur votre téléphone (hors flutter run), et vérifiez qu’il fonctionne sans l’ordinateur connecté (avec votre backend accessible sur le réseau local).

👉 Voir les corrigés complets


6. Projet final de synthèse

Cahier des charges — Application « Gestion Scolaire »

C’est le projet qui rassemble toutes les notions du cours Flutter. Un corrigé complet, prêt à l’emploi, est fourni dans le dossier projet-final/ et je vous donnerai un zip du projet pour simplifier.

  1. Écran de connexion (JWT, Annexe A) avec gestion des erreurs et stockage sécurisé du token ;
  2. Écran Liste des étudiants, connecté à l’API Spring Boot, avec chargement, erreurs, pull-to-refresh, recherche par nom (TextField + filtrage) ;
  3. Écran Détail/Formulaire réutilisé pour créer et modifier un étudiant, avec validation complète ;
  4. Suppression avec confirmation ;
  5. Menu latéral (Drawer) avec accès à un écran « Profil » (affichant le nom de l’utilisateur connecté extrait du token) et un bouton de déconnexion ;
  6. Mode sombre persistant (shared_preferences) ;
  7. Architecture en couches complète (models/services/providers/screens/widgets) ;
  8. Au moins 3 tests unitaires et 1 test de widget ;
  9. Testé sur un vrai téléphone (Android ou iOS).

👉 Explorer le code source complet du projet final 👉 Voir les corrigés détaillés du Jour 5


🎓 Félicitations !

Vous avez terminé ce cours de 5 jours et vous savez désormais :

Prochaines étapes : explorez go_router pour la navigation avancée, riverpod (alternative moderne à Provider), dio pour des appels HTTP plus robustes (intercepteurs, retry), et freezed/json_serializable pour la génération automatique de modèles. Vous pouvez voir cela de votre côté en explorant la documentation de Flutter qui est plutôt bien faite.

Voici un chapitre 6 complémentaire

⬅️ Retour au sommaire Flutter