Meilleures pratiques de revue de code Flutter/Dart
Checklist complète et indépendante de toute bibliothèque 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
- Absence de logique métier dans les widgets ; les widgets sont purement présentationnels
-
pubspec.yamlest propre — pas de dépendances inutilisées, versions épinglées de manière appropriée -
analysis_options.yamlinclut un ensemble de règles strictes avec les paramètres de l'analyseur strict 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 à la plateforme est isolé derrière des abstractions
2. Pièges du langage Dart
- Dynamique implicite : Annotations de type manquantes menant à
dynamic— activerstrict-casts,strict-inference,strict-raw-types - Mauvais usage de la null safety : Utilisation excessive de
!(opérateur bang) au lieu de vérifications null appropriées ou du pattern matching Dart 3 (if (value case var v?)) - Échecs de promotion de type : Utilisation de
this.fieldalors que 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 devraient pas être attrapés -
asyncinutilisé : Fonctions marquéesasyncqui n'utilisent jamaisawait— surcharge inutile - Surabondance de
late:lateutilisé là où une valeur nullable ou une initialisation dans le 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 des classes avec constructeurconstne doivent pas être mutables - Ignorer les valeurs de retour
Future: Utiliserawaitou appeler explicitementunawaited()pour signaler l'intention -
varalors quefinalfonctionne : Préférerfinalpour les variables locales etconstpour les constantes à la compilation - Importations relatives : Utiliser des importations
package:pour la cohérence - Collections mutables exposées : Les API publiques doivent retourner des vues non modifiables, pas des
List/Mapbruts - Pattern matching 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 DTO à usage unique -
print()dans le code de production : Utiliserdart:developerlog()ou le package de journalisation du projet ;print()n'a pas de niveaux de journal et ne peut pas être filtré
3. Meilleures pratiques pour les 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 façon de changer (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 des éléments, la propagation de const et les optimisations du framework) - Les widgets sans état sont préférés aux widgets avec état lorsqu'aucun état mutable local n'est nécessaire
- Les widgets extraits sont dans des fichiers séparés lorsqu'ils sont réutilisables
Utilisation de const :
- Des constructeurs
constsont utilisés partout où c'est possible — empêche les reconstructions inutiles - Des littéraux
constpour les collections qui ne changent pas (const [],const {}) - Le constructeur est déclaré
constlorsque tous les champs sontfinal
Utilisation des clés :
-
ValueKeyutilisé dans les listes/grilles pour préserver l'état lors des réorganisations -
GlobalKeyutilisé avec parcimonie — uniquement lorsque l'accès à l'état à travers l'arbre est vraiment nécessaire -
UniqueKeyévité dansbuild()— il force une reconstruction à chaque image -
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.redou de valeurs hexadécimales en dur - 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 jetons de design cohérents ou des constantes, 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 (indépendante de toute bibliothèque)
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 leurs dépendances via injection, pas en les construisant en interne
- Une couche de service ou de repository 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 seule responsabilité — pas de gestionnaires « dieu » 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 repositories partagés ou la coordination au niveau de la couche de présentation
- Dans les autres solutions : suivre les conventions documentées pour la communication entre composants
- Dans Riverpod : les providers dépendant d'autres providers via
Immutabilité et égalité des valeurs (pour les solutions à état immuable : BLoC, Riverpod, Redux) :
- Les objets d'état sont immuables — de nouvelles instances sont 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 sous forme de
List/Mapbruts mutables
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 de calcul de la solution plutôt que d'être stockées de manière redondante
- Les réactions et les disposers sont correctement nettoyés (
ReactionDisposerdans MobX, nettoyage d'effet 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 transportent des informations d'erreur pour l'affichage ; les états de chargement ne transportent 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) — enum observable + données, mutations via API réactive
// enum UserStatus { initial, loading, loaded, error }
// Utiliser l'observable/signal de votre solution pour envelopper l'état et les données séparément
Optimisation des reconstructions :
- Les widgets consommateurs d'état (Builder, Consumer, Observer, Obx, Watch, etc.) sont limités au périmètre le plus étroit 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 libération :
- 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 minuteries sont annulées dans le cycle de vie de libération
- Le cycle de vie géré par le framework est préféré aux abonnements manuels (builders déclaratifs plutôt que
.listen()) - Vérification de
mountedavantsetStatedans les callbacks asynchrones -
BuildContextnon utilisé aprèsawaitsans vérifiercontext.mounted(Flutter 3.7+) — un contexte obsolète provoque des plantages - Pas de navigation, dialogues ou messages de scaffold après des pauses asynchrones sans vérifier que le widget est toujours monté
-
BuildContextjamais stocké dans des singletons, gestionnaires d'état ou champs statiques
État local vs global :
- L'état UI éphémère (case à cocher, curseur, animation) utilise l'état local (
setState,ValueNotifier) - L'état partagé n'est remonté qu'aussi haut que nécessaire — pas de globalisation excessive
- L'état propre à une fonctionnalité est correctement libéré 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
childdeAnimatedBuilderutilisé pour les sous-arbres indépendants de l'animation
Opérations coûteuses dans build() :
- Pas de tri, filtrage ou 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 placeholder et d'erreur fournis pour les images réseau
Chargement paresseux :
-
ListView.builder/GridView.builderutilisés au lieu deListView(children: [...])pour les listes grandes ou dynamiques (les constructeurs concrets sont acceptables pour les petites listes 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 ==n'est pas surchargé sur les widgets — utiliser les 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, repositories, 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, réessai, 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 définissent uniquement le comportement nécessaire pour chaque test (stubbing minimal)
- Aucun é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 en CI et les échecs bloquent les fusions
7. Accessibilité
Widgets sémantiques :
- Widget
Semanticsutilisé pour fournir des étiquettes de lecteur d'écran lorsque les étiquettes automatiques sont insuffisantes -
ExcludeSemanticsutilisé pour les éléments purement décoratifs -
MergeSemanticsutilisé pour combiner des widgets apparentés en un seul élément accessible - Les images ont la propriété
semanticLabeldéfinie
Support 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 visuelle)
Accessibilité visuelle :
- Ratio 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
onPressedsans opération — 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 :
- Les widgets adaptatifs à la plateforme sont utilisés là où c'est approprié
- La navigation arrière est gérée correctement (bouton retour Android, balayage retour iOS)
- La barre d'état et la zone sécurisée sont gérées via le widget
SafeArea - Les permissions spécifiques à la plateforme sont déclarées dans
AndroidManifest.xmletInfo.plist
Conception réactive :
-
LayoutBuilderouMediaQueryutilisés pour les mises en page réactives - Les points de rupture sont 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
- Web spécifique : interactions souris/clavier supportées, états de survol présents
9. Sécurité
Stockage sécurisé :
- Les données sensibles (jetons, mots de passe, clés API) sont stockées dans un stockage sécurisé (flutter_secure_storage, keychain/keystore)
- Pas de données sensibles dans le code source, les fichiers de configuration ou les préférences partagées
Validation des entrées :
- Toutes les entrées utilisateur sont validées et assainies (empêcher l'injection XSS/NoSQL/commande)
- Les entrées API ne sont pas directement affichées sans échappement approprié (risque XSS dans les applications web)
Communication réseau :
- Toutes les communications API utilisent HTTPS — pas de requêtes HTTP
- Les certificats SSL sont correctement gérés — pas d'épinglage de certificat en dur (utiliser un mécanisme sécurisé comme pinning via secure store ou service worker)
- Les jetons d'authentification sont stockés de manière sécurisée et inclus dans les en-têtes de requête, pas dans les URL
=== SECTION TRONQUÉE POUR LA TRADUCTION ===
(La suite du document original, si elle existe, continuerait ici avec d'autres sections sur les préoccupations de sécurité supplémentaires, le débogage, la journalisation, les considérations de construction et de déploiement, etc. La structure resterait cohérente avec le format de checklist ci-dessus.)
Ce document reflète les meilleures pratiques de la communauté et les recommandations de l'équipe Flutter. Il doit être adapté aux conventions et exigences spécifiques du projet.