S

super-dev — full-stack

@super-dev.app · Full-stack · ★ ouvert aux missions
12,8k abonnés·47 vidéos·Actif depuis 2017

Développeur full-stack .NET / Angular avec une casquette DevOps sur Azure Cloud. Aussi à l'aise sur Flutter + Firebase. Je construis des produits, je les déploie, je les surveille.

Retour aux articles
FLUTTER
FLUTTER

Une architecture Flutter testable avec Riverpod

Article 2 sur 3 — Flutter en production
Offline-first, Riverpod, monorepo Melos : du mobile sérieux, pas un proto.

Une app Flutter qui grossit finit toujours par poser la même question : où vit l'état, et comment le tester sans démarrer un widget ? setState mélange logique et 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 répond aux deux : un conteneur d'injection de dépendances qui produit des valeurs réactives, indépendantes de l'arbre de widgets.

Providers et notifiers

Un Provider expose une valeur ; un Notifier expose une valeur mutable assortie de la logique qui la fait évoluer. Avec la génération de code ( riverpod_generator ), on annote une classe et le build() renvoie l'état initial :

Dart
1@riverpod
2class Counter extends _$Counter {
3 @override
4 int build() => 0;
5
6 void increment() => state = state + 1;
7}
8
9@riverpod
10Future<User> currentUser(CurrentUserRef ref) {
11 final api = ref.watch(apiClientProvider);
12
13 return api.fetchMe();
14}

Le ref.watch à l'intérieur d'un provider crée une dépendance : si apiClientProvider change, currentUser se recalcule automatiquement. C'est le graphe de dépendances qui remplace les setState manuels en cascade.

Séparer l'UI de la logique

La règle d'or : un widget ne contient aucune logique métier. Il lit l'état et appelle des méthodes. Toute la mécanique vit dans le notifier, testable sans WidgetTester :

Dart
1class CounterView extends ConsumerWidget {
2 const CounterView({super.key});
3
4 @override
5 Widget build(BuildContext context, WidgetRef ref) {
6 final count = ref.watch(counterProvider);
7
8 return Text('$count');
9 }
10}

Côté lecture asynchrone, un ref.watch(currentUserProvider) renvoie un AsyncValue , dont le .when(data:, loading:, error:) couvre les trois états sans booléen `isLoading` baladeur .

Injection de dépendances et overrides en test

C'est là que Riverpod surpasse InheritedWidget : chaque provider est remplaçable au montage du ProviderContainer . En test, on injecte un faux client API sans toucher au code de production :

Dart
1test('charge l_utilisateur courant', () async {
2 final container = ProviderContainer(
3 overrides: [
4 apiClientProvider.overrideWithValue(FakeApiClient()),
5 ],
6 );
7 addTearDown(container.dispose);
8
9 final user = await container.read(currentUserProvider.future);
10 expect(user.name, 'Ada');
11});

Pas de mock global, pas de singleton à réinitialiser entre les tests : chaque container est isolé, et addTearDown garantit qu'il est libéré. La documentation des tests Riverpod détaille les patterns de pump et de listeners.

Pourquoi pas setState ou InheritedWidget

setState reconstruit tout le State et garde la logique soudée à l'UI — impossible à tester sans rendre le widget. InheritedWidget partage une valeur mais impose d'écrire à la main le updateShouldNotify , ne gère ni l'asynchrone ni le remplacement, et fuit dès qu'on touche au BuildContext . Riverpod déplace l'état hors de l'arbre , le rend paresseux, mémoïsé et auto-disposé ( autoDispose ), et fait de l'override la voie normale du test.

Une bonne architecture Flutter ne se mesure pas au nombre de providers, mais à ceci : peut-on tester la logique sans jamais monter un widget ? Avec Riverpod, la réponse est oui par construction.
super-dev — portfolio.app
// Continuer dans FLUTTER
Sync offline-first en Flutter + Firebase
9 min • 2,2k lectures
Monorepo Flutter avec Melos
6 min • 1,2k lectures