# Client Flutter fonctionnel en conditions réelles : Trois solutions pour des scénarios complexes
En théorie, l'architecture d'une application Flutter semble parfaite : Bloc pour la gestion d'état, Dio pour l'API, go_router pour la navigation. Dans la pratique, les projets réels rencontrent des problèmes bien au-delà des exemples du manuel – des changements d'utilisateur aux opérations en arrière-plan. Comment bâtir un client qui ne s'effondre pas au premier écart par rapport au scénario nominal ?
Examinons une application d'apprentissage des langues aux fonctionnalités riches et complexes : authentification multi-fournisseurs, connexion anonyme, traduction, reconnaissance vocale, génération de contenu, mode hors ligne et connexions WebSocket. Les packages standards (flutter_bloc, dio, get_it) résolvent des tâches individuelles mais ne couvrent pas les scénarios où la logique métier dépasse les exemples simples. Les principaux problèmes surgissent avec :
- Le lien d'un utilisateur anonyme à un compte permanent sans perte de données
- Le travail avec un réseau nominalement disponible mais instable
- La gestion d'opérations longues (dizaines de secondes) sans bloquer l'interface
- Le changement d'identité correct lors de la déconnexion/connexion
- Le nettoyage garanti de l'état après déconnexion utilisateur
Ces cas nécessitent non seulement de nouveaux packages, mais des solutions architecturales fondamentales. Voici trois approches qui ont permis de créer un client robuste.
Séparation de l'identité et de l'accès à l'application
L'approche standard – utiliser Firebase Auth comme source unique de vérité. Mais avec une logique métier complexe, cela mène à des problèmes. Par exemple, passer d'une connexion anonyme à une inscription Google entraîne une perte de données, et les règles d'accès au serveur deviennent étroitement couplées à Firebase.
Nous avons séparé les responsabilités :
- Firebase gère l'identité (email/mot de passe, Google, Apple, connexion anonyme)
- Le backend génère son propre token JWT après confirmation de l'identité
Flux de travail :
- L'utilisateur se connecte via Firebase
- Le client obtient le token ID Firebase
- L'envoie au backend
- Le backend renvoie le JWT d'application
- Toutes les requêtes suivantes (REST et WebSocket) utilisent ce JWT
Cela résout trois problèmes clés :
- Indépendance des fournisseurs : passer de Firebase à un autre fournisseur d'identité n'affecte pas la logique backend
- Unification du transport : un seul token fonctionne pour HTTP, WebSocket et gestion des permissions
- Lien de compte fluide : l'utilisateur anonyme conserve ses données lors de la mise à niveau vers un compte permanent
Erreur critique ici – essayer d'utiliser directement le token Firebase pour l'accès API. Le backend doit gérer ses propres permissions et cycle de vie des sessions. L'authentification en deux étapes ajoute de la complexité, mais paie en flexibilité et applicabilité réelle.
Réinitialisation complète du contexte au lieu d'un nettoyage manuel
Tenter de vider logiquement l'état après déconnexion (zéro TokenCubit, UserCubit, cache) mène inévitablement à des fuites. Surtout dans les scénarios :
- Changement d'utilisateur (déconnexion → connexion sous un autre compte)
- Lien du profil anonyme à un compte enregistré
- Récupération de session après expiration du token
- Retour d'un écran profondément imbriqué
Au lieu d'un nettoyage ponctuel, nous recréons l'intégralité du contexte des providers. Implémentation technique :
- Dans le widget racine AppInitializer, créer une clé unique
- Lors de changements critiques d'état d'auth (déconnexion, changement d'utilisateur), changer la clé
- Le sous-arbre avec MultiBlocProvider se reconstruit de zéro
- Tous les Cubits s'initialisent via GetIt sur une ardoise vierge
final GlobalKey<_AppInitializerState> appContextKey =
GlobalKey<_AppInitializerState>();
class _AppInitializerState extends State<AppInitializer> {
Key _appKey = UniqueKey();
void resetApp() {
setState(() {
_appKey = UniqueKey();
});
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
key: _appKey,
providers: ProvidersManager.getAllProviders(),
child: widget.child,
);
}
}
Cette méthode est peu élégante mais prévisible. Après réinitialisation, l'app se comporte comme une instance de session fraîche. Important :
- Séparer clairement les services à longue durée de vie (ex. : connexion WebSocket) des états d'écran
- Configurer la DI pour que les singletons ne soient pas affectés par la recréation
- Ne pas utiliser cela pour des apps simples (moins de 5 écrans)
Pour les projets complexes, la réinitialisation complète du contexte est moins coûteuse et plus fiable que la sanitisation manuelle infinie.
Hybride HTTP + WebSocket pour les opérations longues
Le pattern standard final response = await dio.post(...) casse sur les opérations prenant des dizaines de secondes (analyse de texte, génération de contenu, reconnaissance). Problèmes :
- Timeouts des requêtes HTTP
- Instabilité quand l'app est en arrière-plan
- Mauvaise UX pendant les longues attentes
- Diagnostics d'erreur délicats
Solution – passer à un modèle hybride :
- Le client envoie la requête avec l'en-tête
X-Async-Background: true - Le serveur renvoie un
task_idau lieu du résultat final - Le client s'abonne à l'événement de complétion via WebSocket
- Après notification, récupère le résultat via une requête REST séparée
Avantages clés :
- Évolutivité : un point de terminaison rapide peut devenir asynchrone en arrière-plan sans réécriture côté client
- Fiabilité : WebSocket gère le signal de complétion, REST la livraison du résultat
- Diagnostics : points séparés pour le traçage, les tentatives et la gestion d'erreurs
Pour un client mobile, crucial d'implémenter :
- Reconnexion automatique
- Mécanisme de heartbeat
- File d'attente de messages
- Gestion des événements de cycle de vie de l'app
- Streams séparés pour statut, données et erreurs
Cela transforme WebSocket d'un simple « canal de notifications » en une couche d'infrastructure centrale. L'en-tête X-Async-Background sert d'interface à une solution backend prête, pas d'un bricolage client.
Ce qui compte : Points clés à retenir
- Séparez identité et accès : Firebase/Apple/Google confirment l'identité, mais ne gèrent pas les droits d'accès aux ressources
- Réinitialisez le contexte, ne videz pas l'état : lors d'un changement d'utilisateur, recréez l'arbre des providers au lieu d'un nettoyage manuel
- Concevez pour l'évolution : structurez les opérations longues comme HTTP (démarrage) → WebSocket (signal) → HTTP (résultat)
- Testez au-delà du scénario nominal : connexion anonyme → inscription, réseau à haute latence, changement d'utilisateur dans une pile de navigation profonde
- Isolez les couches d'infrastructure : WebSocket comme composant système, pas comme exception
Ces solutions sont nées non de préférences théoriques, mais d'échecs en conditions réelles. Flutter fournit les outils, mais la résilience d'une app dépend de la façon dont vous gérez les frontières entre composants. Se concentrer sur les scénarios où ça déraille est la seule voie pour bâtir un client prêt pour la production.
— Editorial Team
Aucun commentaire pour le moment.