S

super-dev — full-stack

@super-dev.app · Full-stack · ★ offen für Aufträge
12,8k Abonnenten·47 Videos·Aktiv seit 2017

Full-Stack-Entwickler .NET / Angular mit DevOps-Erfahrung auf Azure Cloud. Auch zu Hause mit Flutter + Firebase. Ich entwickle Produkte, deploye sie und überwache sie.

Zurück zu den Artikeln
FLUTTER
FLUTTER

Eine testbare Flutter-Architektur mit Riverpod

Artikel 2 von 3 — Flutter in der Produktion
Offline-first, Riverpod, Melos-Monorepo: ernsthaftes Mobile, kein Prototyp.

Eine Flutter-App, die wächst, wirft am Ende immer dieselbe Frage auf: Wo lebt der Zustand, und wie testet man ihn, ohne ein Widget zu starten? setState vermischt Logik und UI in derselben Klasse; InheritedWidget propagiert die Daten gut, sagt aber nichts über ihre Erstellung oder ihren Austausch im Test. Riverpod beantwortet beides: ein Dependency-Injection-Container, der reaktive Werte erzeugt, unabhängig vom Widget-Baum.

Providers und Notifiers

Ein Provider stellt einen Wert bereit; ein Notifier stellt einen veränderlichen Wert zusammen mit der Logik bereit, die ihn weiterentwickelt. Mit Code-Generierung ( riverpod_generator ) annotiert man eine Klasse, und build() liefert den Anfangszustand:

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}

Das ref.watch innerhalb eines Providers erzeugt eine Abhängigkeit : Ändert sich apiClientProvider , wird currentUser automatisch neu berechnet. Dieser Abhängigkeitsgraph ersetzt manuelle setState -Kaskaden.

UI von Logik trennen

Die Grundregel: Ein Widget enthält keine Geschäftslogik. Es liest den Zustand und ruft Methoden auf. Die gesamte Mechanik lebt im Notifier, testbar ohne 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}

Beim asynchronen Lesen liefert ref.watch(currentUserProvider) einen AsyncValue , dessen .when(data:, loading:, error:) alle drei Zustände abdeckt — ohne herumgereichten `isLoading`-Boolean .

Dependency Injection und Overrides im Test

Hier übertrifft Riverpod InheritedWidget : Jeder Provider ist beim Erstellen des ProviderContainer austauschbar . Im Test injiziert man einen gefälschten API-Client, ohne den Produktionscode anzufassen:

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});

Kein globales Mock, kein Singleton, das zwischen Tests zurückgesetzt werden muss: Jeder Container ist isoliert, und addTearDown stellt sicher, dass er freigegeben wird. Die Riverpod-Testdokumentation beschreibt die pump - und Listener-Patterns im Detail.

Warum nicht setState oder InheritedWidget

setState baut den gesamten State neu auf und koppelt die Logik fest an die UI — ohne Widget-Rendering kein Test möglich. InheritedWidget teilt einen Wert, erfordert aber manuelles Schreiben von updateShouldNotify , unterstützt weder Asynchronität noch Austausch und verliert sich sobald man den BuildContext berührt. Riverpod verschiebt den Zustand aus dem Baum heraus , macht ihn lazy, memoisiert und automatisch freigegeben ( autoDispose ), und macht den Override zum normalen Testweg.

Eine gute Flutter-Architektur misst sich nicht an der Anzahl der Provider, sondern daran: Kann man die Logik testen, ohne jemals ein Widget zu mounten ? Mit Riverpod lautet die Antwort per Konstruktion: ja.
super-dev — portfolio.app
// Weiter in FLUTTER
Offline-First-Sync mit Flutter + Firebase
9 min • 2,2k Aufrufe
Flutter-Monorepo mit Melos
6 min • 1,2k Aufrufe