Aller au contenu principal
Ben Tiga Jean Vincent
Retour au blog
  • 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.

Ben Tiga Jean Vincent5 min de lecture

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 dominantMon choixPourquoi
Livrer vite, équipe réduite, peu d'états partagésGetXNavigation, injection et état en un seul outil, très peu de cérémonie
État partagé complexe, beaucoup de dérivationsRiverpodProviders composables, testables sans widget, invalidation fine
Flux métier à états explicites et auditablesBlocTransitions 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ôleurs

La 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 un setState. 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 :

  1. const partout où c'est possible. Un widget const n'est jamais reconstruit. L'analyseur Dart le signale automatiquement.
  2. ListView.builder, jamais ListView(children: [...]) sur une liste dont la taille n'est pas bornée à l'avance.
  3. 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.
  4. 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 buildNumber dé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 GlobalKey pour 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

4 min de lecture

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
Lire l'article
4 min de lecture

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
Lire l'article