Bonnes Pratiques de Revue de Code Flutter/Dart
Checklist complète et agnostique pour la revue d'applications Flutter/Dart. Ces principes s'appliquent indépendamment de la solution de gestion d'état, de la bibliothèque de routage ou du framework d'injection de dépendances utilisé.
1. Santé Générale du Projet
- Le projet suit une structure de dossiers cohérente (feature-first ou layer-first)
- Séparation correcte des préoccupations : UI, logique métier, couches de données
- Aucune logique métier dans les widgets ; les widgets sont purement présentatifs
-
pubspec.yamlest propre — pas de dépendances inutilisées, versions épinglées de manière appropriée -
analysis_options.yamlinclut un ensemble de lints strict avec les paramètres d'analyse stricts activés - Pas d'instructions
print()dans le code de production — utiliserdart:developerlog()ou un package de journalisation - Les fichiers générés (
.g.dart,.freezed.dart,.gr.dart) sont à jour ou dans.gitignore - Le code spécifique à une plateforme est isolé derrière des abstractions
2. Pièges du Langage Dart
-
dynamicimplicite : Annotations de type manquantes conduisant àdynamic— activerstrict-casts,strict-inference,strict-raw-types - Mauvaise utilisation de la null safety : Utilisation excessive de
!(opérateur bang) au lieu de vérifications null appropriées ou du filtrage par motif Dart 3 (if (value case var v?)) - Échecs de promotion de type : Utilisation de
this.fieldlà où la promotion de variable locale fonctionnerait - Attraper trop largement :
catch (e)sans clauseon; toujours spécifier les types d'exceptions - Attraper
Error: Les sous-types deErrorindiquent des bugs et ne doivent pas être attrapés -
asyncinutilisé : Fonctions marquéesasyncqui n'utilisent jamaisawait— surcharge inutile - Surcharge de
late:lateutilisé là où nullable ou l'initialisation par constructeur serait plus sûre ; reporte les erreurs à l'exécution - Concaténation de chaînes dans les boucles : Utiliser
StringBufferau lieu de+pour la construction itérative de chaînes - État mutable dans les contextes
const: Les champs dans les classes de constructeurconstne doivent pas être mutables - Ignorer les valeurs de retour
Future: Utiliserawaitou appeler explicitementunawaited()pour signaler l'intention -
varlà oùfinalfonctionne : Préférerfinalpour les variables locales etconstpour les constantes de compilation - Imports relatifs : Utiliser des imports
package:pour la cohérence - Collections mutables exposées : Les API publiques doivent retourner des vues non modifiables, pas des
List/Mapbruts - Filtrage par motif Dart 3 manquant : Préférer les expressions switch et
if-caseaux vérificationsisverbeuses et au casting manuel - Classes jetables pour les retours multiples : Utiliser les enregistrements Dart 3
(String, int)au lieu de DTOs à usage unique -
print()dans le code de production : Utiliserdart:developerlog()ou le package de journalisation du projet ;print()n'a pas de niveaux de log et ne peut pas être filtré
3. Bonnes Pratiques des Widgets
Décomposition des widgets :
- Aucun widget avec une méthode
build()dépassant ~80-100 lignes - Widgets divisés par encapsulation ET par leur mode de changement (limites de reconstruction)
- Les méthodes d'aide privées
_build*()qui retournent des widgets sont extraites dans des classes de widgets séparées (permet la réutilisation d'éléments, la propagation de const et les optimisations du framework) - Les widgets Stateless sont préférés aux Stateful là où aucun état local mutable n'est nécessaire
- Les widgets extraits sont dans des fichiers séparés lorsqu'ils sont réutilisables
Utilisation de const :
- Les constructeurs
constsont utilisés autant que possible — évite les reconstructions inutiles - Littéraux
constpour les collections qui ne changent pas (const [],const {}) - Le constructeur est déclaré
constlorsque tous les champs sont finaux
Utilisation des clés :
-
ValueKeyutilisé dans les listes/grilles pour préserver l'état lors des réorganisations -
GlobalKeyutilisé avec parcimonie — seulement lorsque l'accès à l'état à travers l'arbre est vraiment nécessaire -
UniqueKeyévité dansbuild()— il force une reconstruction à chaque frame -
ObjectKeyutilisé lorsque l'identité est basée sur un objet de données plutôt que sur une valeur unique
Thème et système de design :
- Les couleurs proviennent de
Theme.of(context).colorScheme— pas deColors.redcodé en dur ni de valeurs hexadécimales - Les styles de texte proviennent de
Theme.of(context).textTheme— pas deTextStyleen ligne avec des tailles de police brutes - La compatibilité avec le mode sombre est vérifiée — pas d'hypothèses sur un fond clair
- L'espacement et le dimensionnement utilisent des tokens de design ou des constantes cohérents, pas de nombres magiques
Complexité de la méthode build :
- Pas d'appels réseau, d'E/S fichier ou de calculs lourds dans
build() - Pas de
Future.then()ou de travailasyncdansbuild() - Pas de création d'abonnement (
.listen()) dansbuild() -
setState()localisé au plus petit sous-arbre possible
4. Gestion d'État (Agnostique aux Bibliothèques)
Ces principes s'appliquent à toutes les solutions de gestion d'état Flutter (BLoC, Riverpod, Provider, GetX, MobX, Signals, ValueNotifier, etc.).
Architecture :
- La logique métier vit en dehors de la couche widget — dans un composant de gestion d'état (BLoC, Notifier, Controller, Store, ViewModel, etc.)
- Les gestionnaires d'état reçoivent les dépendances via injection, pas en les construisant en interne
- Une couche de service ou de référentiel abstrait les sources de données — les widgets et les gestionnaires d'état ne doivent pas appeler les API ou les bases de données directement
- Les gestionnaires d'état ont une responsabilité unique — pas de gestionnaires «dieux» gérant des préoccupations non liées
- Les dépendances entre composants suivent les conventions de la solution :
- Dans Riverpod : les providers dépendant d'autres providers via
ref.watchest attendu — signaler uniquement les chaînes circulaires ou trop emmêlées - Dans BLoC : les blocs ne doivent pas dépendre directement d'autres blocs — préférer les référentiels partagés ou la coordination au niveau de la couche de présentation
- Dans d'autres solutions : suivre les conventions documentées pour la communication entre composants
- Dans Riverpod : les providers dépendant d'autres providers via
Immuabilité et égalité des valeurs (pour les solutions à état immuable : BLoC, Riverpod, Redux) :
- Les objets d'état sont immuables — de nouvelles instances créées via
copyWith()ou des constructeurs, jamais mutées sur place - Les classes d'état implémentent correctement
==ethashCode(tous les champs inclus dans la comparaison) - Le mécanisme est cohérent dans tout le projet — remplacement manuel,
Equatable,freezed, enregistrements Dart, ou autre - Les collections à l'intérieur des objets d'état ne sont pas exposées comme des
List/Mapmutables bruts
Discipline de réactivité (pour les solutions à mutation réactive : MobX, GetX, Signals) :
- L'état n'est muté qu'à travers l'API réactive de la solution (
@actiondans MobX,.valuesur les signaux,.obsdans GetX) — la mutation directe de champ contourne le suivi des changements - Les valeurs dérivées utilisent le mécanisme calculé de la solution plutôt que d'être stockées de manière redondante
- Les réactions et les dispositifs de nettoyage sont correctement nettoyés (
ReactionDisposerdans MobX, nettoyage d'effets dans Signals)
Conception de la forme de l'état :
- Les états mutuellement exclusifs utilisent des types scellés, des variantes d'union, ou le type d'état asynchrone intégré de la solution (par exemple
AsyncValuede Riverpod) — pas de drapeaux booléens (isLoading,isError,hasData) - Chaque opération asynchrone modélise le chargement, le succès et l'erreur comme des états distincts
- Toutes les variantes d'état sont traitées de manière exhaustive dans l'UI — pas de cas ignorés silencieusement
- Les états d'erreur portent des informations d'erreur pour l'affichage ; les états de chargement ne portent pas de données obsolètes
- Les données nullables ne sont pas utilisées comme indicateur de chargement — les états sont explicites
// MAUVAIS — la soupe de drapeaux booléens permet des états impossibles
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasError est représentable !
User? user;
}
// BON (approche immuable) — les types scellés rendent les états impossibles non représentables
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
const UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
const UserError(this.message);
}
// BON (approche réactive) — observable enum + données, mutations via l'API de réactivité
// enum UserStatus { initial, loading, loaded, error }
// Utilisez l'observable/signal de votre solution pour envelopper le statut et les données séparément
Optimisation des reconstructions :
- Les widgets consommateurs d'état (Builder, Consumer, Observer, Obx, Watch, etc.) sont délimités aussi étroitement que possible
- Les sélecteurs sont utilisés pour ne reconstruire que lorsque des champs spécifiques changent — pas à chaque émission d'état
- Les widgets
constsont utilisés pour arrêter la propagation des reconstructions dans l'arbre - L'état calculé/dérivé est calculé de manière réactive, pas stocké de manière redondante
Abonnements et élimination :
- Tous les abonnements manuels (
.listen()) sont annulés dansdispose()/close() - Les contrôleurs de flux sont fermés lorsqu'ils ne sont plus nécessaires
- Les minuteurs sont annulés dans le cycle de vie de l'élimination
- Le cycle de vie géré par le framework est préféré à l'abonnement manuel (builders déclaratifs plutôt que
.listen()) - Vérification
mountedavantsetStatedans les callbacks asynchrones -
BuildContextnon utilisé aprèsawaitsans vérifiercontext.mounted(Flutter 3.7+) — un contexte obsolète provoque des crashes - Pas de navigation, de dialogues ou de messages de scaffold après des pauses asynchrones sans vérifier que le widget est toujours monté
-
BuildContextjamais stocké dans des singletons, des gestionnaires d'état ou des champs statiques
État local vs global :
- L'état UI éphémère (checkbox, slider, animation) utilise l'état local (
setState,ValueNotifier) - L'état partagé est remonté uniquement aussi haut que nécessaire — pas de sur-globalisation
- L'état limité à une fonctionnalité est correctement éliminé lorsque la fonctionnalité n'est plus active
5. Performances
Reconstructions inutiles :
-
setState()n'est pas appelé au niveau du widget racine — localiser les changements d'état - Les widgets
constsont utilisés pour arrêter la propagation des reconstructions -
RepaintBoundaryutilisé autour de sous-arbres complexes qui se repeignent indépendamment - Le paramètre
childdeAnimatedBuilderest utilisé pour les sous-arbres indépendants de l'animation
Opérations coûteuses dans build() :
- Pas de tri, de filtrage ou de mappage de grandes collections dans
build()— calculer dans la couche de gestion d'état - Pas de compilation de regex dans
build() - L'utilisation de
MediaQuery.of(context)est spécifique (par exemple,MediaQuery.sizeOf(context))
Optimisation des images :
- Les images réseau utilisent la mise en cache (toute solution de mise en cache appropriée pour le projet)
- Résolution d'image appropriée pour l'appareil cible (pas de chargement d'images 4K pour les vignettes)
-
Image.assetaveccacheWidth/cacheHeightpour décoder à la taille d'affichage - Widgets de remplacement et d'erreur fournis pour les images réseau
Chargement différé :
-
ListView.builder/GridView.builderutilisé au lieu deListView(children: [...])pour les listes grandes ou dynamiques (les constructeurs concrets sont acceptables pour les listes petites et statiques) - Pagination implémentée pour les grands ensembles de données
- Chargement différé (
deferred as) utilisé pour les bibliothèques lourdes dans les builds web
Autres :
- Widget
Opacityévité dans les animations — utiliserAnimatedOpacityouFadeTransition - Clipping évité dans les animations — pré-clipper les images
-
operator ==non surchargé sur les widgets — utiliser des constructeursconstà la place - Widgets de dimensions intrinsèques (
IntrinsicHeight,IntrinsicWidth) utilisés avec parcimonie (passage de layout supplémentaire)
6. Tests
Types de tests et attentes :
- Tests unitaires : Couvrent toute la logique métier (gestionnaires d'état, référentiels, fonctions utilitaires)
- Tests de widgets : Couvrent le comportement individuel des widgets, les interactions et la sortie visuelle
- Tests d'intégration : Couvrent les flux utilisateur critiques de bout en bout
- Tests golden : Comparaisons pixel-parfaites pour les composants UI critiques pour le design
Objectifs de couverture :
- Viser 80%+ de couverture de lignes sur la logique métier
- Toutes les transitions d'état ont des tests correspondants (chargement → succès, chargement → erreur, nouvelle tentative, etc.)
- Cas limites testés : états vides, états d'erreur, états de chargement, valeurs limites
Isolation des tests :
- Les dépendances externes (clients API, bases de données, services) sont mockées ou simulées
- Chaque fichier de test teste exactement une classe/unité
- Les tests vérifient le comportement, pas les détails d'implémentation
- Les stubs ne définissent que le comportement nécessaire pour chaque test (stub minimal)
- Pas d'état mutable partagé entre les cas de test
Qualité des tests de widgets :
-
pumpWidgetetpumputilisés correctement pour les opérations asynchrones -
find.byType,find.text,find.byKeyutilisés de manière appropriée - Pas de tests instables dépendant du timing — utiliser
pumpAndSettleoupump(Duration)explicite - Les tests s'exécutent dans le CI et les échecs bloquent les fusions
7. Accessibilité
Widgets sémantiques :
- Widget
Semanticsutilisé pour fournir des étiquettes de lecteur d'écran là où les étiquettes automatiques sont insuffisantes -
ExcludeSemanticsutilisé pour les éléments purement décoratifs -
MergeSemanticsutilisé pour combiner des widgets liés en un seul élément accessible - Les images ont la propriété
semanticLabeldéfinie
Prise en charge du lecteur d'écran :
- Tous les éléments interactifs sont focusables et ont des descriptions significatives
- L'ordre de focus est logique (suit l'ordre de lecture visuel)
Accessibilité visuelle :
- Rapport de contraste >= 4.5:1 pour le texte par rapport à l'arrière-plan
- Les cibles tactiles font au moins 48x48 pixels
- La couleur n'est pas le seul indicateur d'état (utiliser des icônes/texte en complément)
- Le texte s'adapte aux paramètres de taille de police du système
Accessibilité des interactions :
- Pas de callbacks
onPressedinopérants — chaque bouton fait quelque chose ou est désactivé - Les champs d'erreur suggèrent des corrections
- Le contexte ne change pas de manière inattendue pendant que l'utilisateur saisit des données
8. Préoccupations Spécifiques à la Plateforme
Différences iOS/Android :
- Widgets adaptatifs à la plateforme utilisés là où approprié
- La navigation arrière est gérée correctement (bouton retour Android, balayage pour revenir en arrière iOS)
- La barre d'état et la zone de sécurité sont gérées via le widget
SafeArea - Les permissions spécifiques à la plateforme sont déclarées dans
AndroidManifest.xmletInfo.plist
Design responsive :
-
LayoutBuilderouMediaQueryutilisé pour les mises en page responsives - Points de rupture définis de manière cohérente (téléphone, tablette, bureau)
- Le texte ne déborde pas sur les petits écrans — utiliser
Flexible,Expanded,FittedBox - L'orientation paysage est testée ou explicitement verrouillée
- Spécifique au web : interactions souris/clavier prises en charge, états de survol présents
9. Sécurité
Stockage sécurisé :
- Les données sensibles (tokens, mots de passe) ne sont pas stockées dans des préférences partagées non cryptées
- Utiliser
flutter_secure_storage(ou équivalent) pour les tokens et secrets - Les données sensibles ne sont pas codées en dur dans le code ou le
pubspec.yaml
Risques réseau/serveur :
- Les requêtes réseau utilisent HTTPS — pas de requêtes HTTP non sécurisées
- Les en-têtes de sécurité (CORS, Content-Security-Policy, HSTS) sont vérifiés pour les builds web
- La validation d'entrée se produit à la fois côté client et côté serveur — la validation côté client uniquement est insuffisante
- Les clés API et autres secrets ne sont pas exposés dans les dépôts git (
.envou variables d'environnement, pas de fichiers commités) - Les jetons et cookies qui expirent sont gérés élégamment — déconnexion ou renouvellement
Prévention des injections :
- Les entrées utilisateur sont échappées avant d'être rendues dans des vues Web ou des interfaces natives — pas d'injection d'HTML, SQL ou commande
- La construction de requêtes SQL utilise des requêtes paramétrées ou ORM, pas de concaténation de chaînes
Validation des entrées :
- Les limites de longueur sont appliquées sur les champs de texte pour éviter les débordements de mémoire
- Les types de fichiers sont validés par le backend, pas seulement par l'extension de fichier côté client
10. Recommandations Supplémentaires
Documentation et commentaires :
- Les API publiques ont des commentaires de documentation (
///) - Les correctifs ou solutions de contournement non évidents ont des commentaires expliquant pourquoi l'approche a été choisie
- Les commentaires
TODOouFIXMEont des tickets de suivi associés ou des propriétaires - Les fonctions complexes ou non triviales ont des commentaires décrivant le comportement attendu
Gestion des erreurs :
- Les exceptions sont attrapées à la limite de la couche de service, pas enfouies dans les gestionnaires UI
- Les messages d'erreur présentés aux utilisateurs sont conviviaux et exploitables, pas des traces de pile
- Les erreurs de l'état d'échec sont enregistrées (en utilisant le système de journalisation du projet) pour le débogage
- Les échecs de récupération d'état ont des mécanismes de nouvelle tentative (retry bouton, pull-to-refresh)
- Les opérations critiques sont validées avec des mécanismes de mise hors service (circuit breaker) pour éviter des échecs en cascade
Organisation des imports :
- Les imports sont regroupés (dart:, package:, relative) avec des lignes vides entre les groupes
- Aucun import inutilisé — exécuter
dart fix --applyou utiliser les actions rapides de l'IDE - Les imports sont triés par ordre alphabétique dans les groupes pour des raisons de cohérence
Cette checklist s'inspire des principes de Clean Architecture, des directives officielles de Flutter et de l'expérience pratique de la communauté.