- Flutter
- Dart
- Mobile
- Architecture
Ma méthode pour une application Flutter prête pour la production
Choix de la gestion d'état, architecture en couches, temps réel et publication sur les stores : ce que j'applique sur mes applications Flutter depuis Woila et Djangi.
Flutter permet d'arriver très vite à un écran convaincant. Le travail commence après : quand l'application doit survivre à un réseau instable, à un suivi GPS continu, et à la revue de l'App Store. Voici la méthode que j'applique depuis Woila et Djangi.
Choisir sa gestion d'état sur des critères, pas par habitude
J'ai livré en production avec GetX, Riverpod et Bloc. Aucun des trois n'est « le meilleur » : ils répondent à des contraintes différentes.
| Besoin dominant | Mon choix | Pourquoi |
|---|---|---|
| Livrer vite, équipe réduite, peu d'états partagés | GetX | Navigation, injection et état en un seul outil, très peu de cérémonie |
| État partagé complexe, beaucoup de dérivations | Riverpod | Providers composables, testables sans widget, invalidation fine |
| Flux métier à états explicites et auditables | Bloc | Transitions tracées, excellent sur les parcours de paiement |
Sur Djangi, les cycles de cotisation ont des transitions strictes : cotisation ouverte, pot attribué, pénalité appliquée. Un Bloc rend ces transitions lisibles et rejouables dans les tests. À l'inverse, une application vitrine n'a aucun besoin de cette rigueur.
Le vrai critère : combien d'états différents une même donnée peut-elle prendre, et qui a le droit de les changer ? Si la réponse tient en une phrase, prends le plus léger.
Trois couches, une seule direction de dépendance
lib/
├── core/ # erreurs, réseau, thème, extensions
└── features/
└── courses/
├── data/ # DTO, client API, cache local
├── domain/ # entités, cas d'usage, zéro import Flutter
└── presentation/ # écrans, widgets, contrôleursLa règle qui donne sa valeur au découpage : domain/ n'importe jamais package:flutter. Cette couche ne connaît ni les widgets, ni le BuildContext, ni le client HTTP. Conséquence directe, elle se teste en Dart pur, sans WidgetTester, donc en quelques millisecondes.
// domain/usecases/calculer_tarif.dart
class CalculerTarif {
const CalculerTarif(this._grille);
final GrilleTarifaire _grille;
Montant call({required double km, required Duration duree, required bool heurePointe}) {
final base = _grille.prixBase + (_grille.prixKm * km) + (_grille.prixMinute * duree.inMinutes);
return Montant.fcfa(heurePointe ? base * _grille.coefficientPointe : base);
}
}Les règles tarifaires de Woila vivent ici. Elles ont changé une dizaine de fois sans qu'un seul widget soit modifié.
Le réseau échoue, ce n'est pas un cas limite
Sur une application de VTC utilisée en déplacement, la coupure réseau est l'état normal plusieurs fois par trajet. Deux décisions découlent de ce constat.
Un type de résultat explicite, pour que l'échec ne puisse pas être oublié :
sealed class Resultat<T> {
const Resultat();
}
final class Succes<T> extends Resultat<T> {
const Succes(this.valeur);
final T valeur;
}
final class Echec<T> extends Resultat<T> {
const Echec(this.erreur);
final ErreurApp erreur;
}Avec les classes scellées de Dart 3, un switch non exhaustif devient une erreur de compilation. Le chemin d'échec ne peut plus être oublié par distraction.
Une file d'attente locale pour les actions qui ne doivent pas être perdues : une course terminée, une cotisation enregistrée. L'action est écrite en base locale, puis rejouée dès le retour du réseau. L'utilisateur ne doit jamais perdre une action parce que le réseau a disparu.
Temps réel : sortir le travail du thread principal
Le suivi GPS de Woila émet une position toutes les quelques secondes, côté chauffeur comme côté passager. Trois points ont fait toute la différence :
- Le flux WebSocket alimente un
Stream, jamais directement unsetState. L'interface écoute, elle ne pilote pas. - Les traitements coûteux partent dans un
Isolate: décodage de polyline, calcul de distance sur un historique. Le thread UI reste disponible pour l'animation. - Les positions sont agrégées avant affichage. Rafraîchir la carte à chaque trame est inutile : l'œil ne distingue rien au-delà de deux mises à jour par seconde, mais la batterie, elle, le sent passer.
Les détails de performance qui comptent vraiment
Quatre réflexes suffisent à couvrir l'essentiel :
constpartout où c'est possible. Un widgetconstn'est jamais reconstruit. L'analyseur Dart le signale automatiquement.ListView.builder, jamaisListView(children: [...])sur une liste dont la taille n'est pas bornée à l'avance.- Mettre les images distantes en cache et leur donner une taille explicite. Une image non dimensionnée provoque un recalcul de mise en page à chaque chargement.
- Isoler ce qui se reconstruit. Un chronomètre qui se rafraîchit chaque seconde doit être son propre widget, pas un morceau de l'écran entier.
La publication est un projet à part entière
C'est l'étape la plus sous-estimée, et de loin celle qui m'a coûté le plus de temps sur Bepmarket.
- Les certificats iOS expirent. Un certificat de distribution et un profil de provisionnement ont une date de fin. Elle tombe toujours au mauvais moment si personne ne la surveille.
- L'App Store rejette sur des motifs non techniques : politique de confidentialité absente, compte de démonstration manquant, fonctionnalité jugée incomplète. Prépare un compte de test fonctionnel et des notes de revue explicites dès la première soumission.
- Google Play exige la déclaration des données collectées. Localisation, identifiants, données de paiement : la section « Sécurité des données » doit correspondre exactement à ce que fait l'application.
- Numéroter les builds automatiquement. Un
buildNumberdérivé du compteur de CI évite la soumission refusée pour cause de version déjà utilisée.
Ce que je ne fais plus
- Appeler l'API depuis un widget. Même pour « un seul appel rapide ». Il n'y a jamais un seul appel.
- Un
GlobalKeypour contourner un problème d'état. C'est le signal qu'un état est mal placé dans l'arbre. - Traduire des chaînes en dur a posteriori. L'internationalisation se branche au premier écran, pas au cinquantième.
Le test que je fais passer avant toute mise en production : activer le mode avion en pleine utilisation. Si l'application affiche un message clair et reprend proprement au retour du réseau, elle est prête. Sinon, le reste n'a pas d'importance.
Articles liés
Intégrer les API de paiement des agrégateurs sans se faire piéger
Mobile Money, cartes bancaires, agrégateurs multiples : le modèle unifié, l'idempotence et la réconciliation qui évitent les doubles débits en production.
- Paiement
- FinTech
- Architecture
- API
Ma méthode pour un front-end Angular qui reste maintenable
Découpage par domaine, composants standalone, signaux contre RxJS, intercepteurs et détection de changement : les décisions que je prends au démarrage d'un projet Angular.
- Angular
- TypeScript
- Architecture
- Frontend