Une architecture Flutter testable avec Riverpod
setState soude la logique à l'UI dans la même classe. InheritedWidget propage bien la donnée mais ne dit rien de sa création ni de son remplacement en test. Riverpod traite les deux problèmes d'un coup : c'est un conteneur d'injection de dépendances qui produit des valeurs réactives, déclarées hors de l'arbre de widgets et reliées entre elles par un graphe typé.
Trois familles de providers
provider , et trois formes couvrent la quasi-totalité des besoins. Un Provider renvoie une valeur immuable (un client HTTP, un dépôt, une configuration). Un NotifierProvider associe un état mutable à la logique qui le fait évoluer. Un AsyncNotifierProvider fait la même chose quand l'état initial est asynchrone : un appel réseau, une lecture de base.
@riverpod , le générateur produit le provider, et le build() renvoie l'état de départ.
todoRepositoryProvider , counterProvider , todoListProvider . C'est cet identifiant que le reste de l'app consomme, jamais la classe directement.
Le graphe de dépendances
ref.watch à l'intérieur d'un provider crée une dépendance . todoListProvider observe todoRepositoryProvider , qui observe httpClientProvider . Si l'un change, tout ce qui en dépend se recalcule, sans câblage manuel en cascade. C'est un graphe orienté, et comme chaque provider a un type de retour connu à la compilation, une erreur de branchement devient une erreur de compilation plutôt qu'un crash à l'exécution.
ref se décline en trois usages qu'il faut distinguer. ref.watch s'abonne : sa place est dans un build() , de widget ou de provider, pour réagir au changement. ref.read lit une fois, sans abonnement : on l'utilise dans un gestionnaire d'événement, pour déclencher une méthode. ref.listen enregistre un effet de bord (afficher une erreur, naviguer) sans reconstruire.
.notifier donne l'objet qui porte les méthodes. Confondre les deux, par exemple watcher un notifier dans un callback, reste l'erreur la plus fréquente chez les nouveaux venus.
Lire l'asynchrone sans booléen baladeur
AsyncNotifierProvider ne renvoie pas un Future brut mais un AsyncValue , un type somme qui encode les trois états possibles : donnée, chargement, erreur. Le .when(data:, loading:, error:) force à traiter les trois, ce qui supprime le booléen isLoading qu'on oublie de remettre à false .
AsyncValue.guard , qui capture l'exception et la range dans un AsyncError au lieu de la laisser remonter.
AsyncValue , et son .when affiche le spinner pendant le rechargement, puis la liste, puis un message si l'écriture a échoué.
Des variables globales qui n'en sont pas
final au niveau du fichier, ce qui ressemble à une variable globale. La ressemblance s'arrête au premier coup d'œil. L'identifiant sert de clé ; l'état réel vit dans un ProviderContainer . Deux containers, deux tests ou deux fenêtres, ne partagent aucun état même s'ils lisent le même provider.
@riverpod rend le provider autoDispose par défaut : dès que plus rien ne l'écoute, son état est détruit, puis recréé paresseusement à la prochaine lecture. Pour garder un état en vie, un cache ou une session, on l'annote @Riverpod(keepAlive: true) . Et ref.onDispose libère une ressource, par exemple fermer un socket, au moment du nettoyage.
L'override comme voie normale du test
InheritedWidget : chaque provider est remplaçable au montage du container. En test, on injecte un faux dépôt sans toucher au code de production.
ProviderContainer est isolé, et addTearDown garantit sa libération. Pour un test de widget, le même mécanisme passe par ProviderScope(overrides: [...]) au sommet de l'arbre. La documentation des tests Riverpod détaille les patterns de pump et d'écoute.
Découper une feature
Provider . La couche logique tient les notifiers, qui watch ent ces dépôts. La couche présentation ne contient que des ConsumerWidget qui watch ent les notifiers et appellent leurs méthodes. Un widget ne porte aucune logique métier : il lit et il délègue.
ref.watch au lieu d'un constructeur. Le test suit la même frontière : on remplace la couche du dessous par un override, et on vérifie celle du dessus, isolément.
Une architecture Flutter se mesure à sa testabilité : la logique doit se vérifier sans monter un seul widget. Riverpod place l'état hors de l'arbre et fait de l'override un mécanisme de première classe, ce qui rend cette vérification possible par défaut.