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…
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 :
⚠️ 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.
shared_preferences
flutter_secure_storage
Voici ci-dessous, comment procéder :
dependencies: flutter_secure_storage: ^9.2.2
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.
write()
read()
delete()
TokenStorage
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); }
Notre classe AuthService va utiliser notre TokenStorage créé précédemment.
AuthService
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; }
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 !
Interceptor
OncePerRequestFilter
Il faut aussi penser à gérer les exceptions.
final response = await requeteAuthentifiee(url); if (response.statusCode == 401) { await AuthService().logout(); // on redirige vers l'écran de connexion }
Comme JUnit côté Spring Boot, Flutter fournit un framework de test complet, avec 3 niveaux :
@Test
@Service
test
@Controller
flutter_test
@SpringBootTest
integration_test
// 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
// 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); }); }
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.
@Repository
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.
android/key.properties
android/app/build.gradle
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).
Des services comme Codemagic permettent de :
flutter_launcher_icons
applicationId
Bundle Identifier
print
10.0.2.2
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).
widgets/
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.
AuthService.login()
Navigator.pushReplacement
É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.
Etudiant
Pour vous aider :
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).
flutter run
👉 Voir les corrigés complets
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.
projet-final/
TextField
Drawer
models/services/providers/screens/widgets
👉 Explorer le code source complet du projet final 👉 Voir les corrigés détaillés du Jour 5
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.
go_router
riverpod
dio
freezed
json_serializable
Voici un chapitre 6 complémentaire
⬅️ Retour au sommaire Flutter