Flutter: Modernes State Management mit Riverpod 3
Zuletzt aktualisiert:
9 Min. Lesezeit

Riverpod ist eine Lösung für State Management von Remi Rousselet, dem Autor des Pakets Provider. Sie wurde entworfen, um die Einschränkungen zu beseitigen, die daher rühren, dass Provider auf InheritedWidget aufbaut: Fehlende Provider fallen schon beim Kompilieren auf, Provider lassen sich leicht kombinieren, und Tests werden einfacher. Die vollständigen Installationsschritte und die „Hello world“-Beispiele stehen im offiziellen Einstiegsleitfaden von Riverpod.
Dieser Beitrag bezieht sich auf Riverpod 3 (flutter_riverpod 3.x). Viele Beispiele im Netz wurden für Riverpod 2 geschrieben und verwenden StateProvider oder StateNotifier. Diese gibt es in Riverpod 3 noch, sie wurden aber in einen eigenen „Legacy“-Import verschoben; gegen Ende des Beitrags zeige ich, wie Sie solchen älteren Code lesen und aktualisieren.
Warum ist State Management wichtig?
Moderne Anwendungen haben komplexe Datenflüsse:
- Anmeldung und Sitzungsverwaltung
- Sofortnachrichten und Benachrichtigungen
- Offline-Datenabgleich
- Mehrere API-Anbindungen
- Aktualisierungen in Echtzeit
Wenn mehrere Bildschirme dieselben Daten lesen und Daten voneinander abhängen, braucht der Zustand eine klare Struktur.
Die Grenzen von Provider
Provider ist ein gutes Werkzeug; wie es mit ChangeNotifier, Consumer und MultiProvider arbeitet, beschreibt der Beitrag zu Provider. Die Riverpod-Dokumentation nennt folgende Einschränkungen von Provider als Grund für Riverpod:
1. Lesen erfordert einen BuildContext
// Provider baut auf InheritedWidget auf: zum Lesen braucht man einen Context
final user = Provider.of<UserProvider>(context);Auch einen anderen Provider mit context.watch innerhalb von create zu lesen, ist nicht zuverlässig.
2. Fehlende Provider fallen erst zur Laufzeit auf
// Fehlt der Provider im Baum, scheitert die App WÄHREND DER AUSFÜHRUNG
final data = context.read<SomeProvider>(); // ProviderNotFoundExceptionDieser Fehler tritt besonders häufig bei Refactorings auf.
3. Provider zu kombinieren ist mühsam
Hängt ein Provider von einem anderen ab, braucht man Konstrukte wie ProxyProvider, und der Code wird schnell unübersichtlich.
4. Zwei Provider desselben Typs sind unzuverlässig
Deklarieren Sie zwei getrennte Provider<Item>, findet InheritedWidget nur einen davon.
5. Kein automatisches Aufräumen, keine Parameter
Provider erkennt nicht, wann niemand mehr auf einen Wert hört, und kann ungenutzten Zustand deshalb nicht selbst freigeben. Einen Provider, der je nach Parameter (etwa einer Produkt-ID) einen anderen Wert liefert, sieht Provider ebenfalls nicht vor.
Wie löst Riverpod diese Probleme?
✅ Provider hängen nicht am Widget-Baum, aber Sie brauchen ein ref
In Riverpod werden Provider als globale Variablen deklariert, ihr Zustand lebt im ProviderScope ganz oben in der App. Zum Lesen verwenden Sie statt eines BuildContext ein ref. Das heißt nicht „Zugriff von überall ohne Context“; woher das ref kommt, hängt vom Ort ab:
// 1) In einem Widget: das WidgetRef aus dem ConsumerWidget
final user = ref.watch(userProvider);
// 2) In einem anderen Provider: dessen eigenes Ref
final greetingProvider = Provider<String>((ref) {
final name = ref.watch(userNameProvider);
return 'Hallo $name';
});
// 3) Außerhalb des Widget-Baums (Tests, Dart-Skripte): ein ProviderContainer
final container = ProviderContainer();
final name = container.read(userNameProvider);
container.dispose();Aus einer beliebigen Service-Klasse heraus können Sie keinen Provider lesen; die Klasse muss entweder selbst ein Provider sein oder die benötigten Werte von außen erhalten. Der eigentliche Gewinn: Provider können sich gegenseitig mit ref.watch lesen, ganz ohne Widget-Baum.
✅ Fehlende Provider fallen beim Kompilieren auf
// Ein Provider ist eine Variable: falsch geschrieben oder nicht definiert heißt, der Code kompiliert NICHT
final data = ref.watch(someProvider);Eine ProviderNotFoundException gibt es in Riverpod nicht. Einzige Voraussetzung: Die Wurzel der App muss in einem ProviderScope liegen; vergessen Sie das, gibt es beim ersten Lesen einen Laufzeitfehler.
✅ Provider einfach kombinieren
// Ein Provider kann andere Provider beobachten
final userOrdersProvider = FutureProvider<List<Order>>((ref) async {
final user = await ref.watch(userProvider.future);
return fetchOrders(user.id);
});Ändert sich userProvider, wird userOrdersProvider automatisch neu berechnet.
✅ Testbarkeit
// Im Test Fake-Daten statt der echten API
final container = ProviderContainer.test(
overrides: [
userProvider.overrideWith((ref) async => fakeUser),
],
);ProviderContainer.test() kam mit Riverpod 3 und räumt sich am Ende des Tests selbst auf. In Widget-Tests übergeben Sie dieselbe overrides-Liste an ProviderScope.
✅ Speicherverwaltung mit autoDispose
Ein mit .autoDispose deklarierter Provider verwirft seinen Zustand, sobald niemand mehr zuhört.
Was hat sich in Riverpod 3 geändert?
Wer von Riverpod 2 kommt, sollte diese Änderungen kennen (siehe die offizielle Seite „What's new“):
NotifierundAsyncNotifiersind der empfohlene Weg.StateProvider,StateNotifierProviderundChangeNotifierProviderwerden jetzt auspackage:flutter_riverpod/legacy.dartimportiert und für neuen Code nicht mehr empfohlen.- AutoDispose- und Family-Klassen wurden zusammengeführt. Es gibt keine eigenen Klassen wie
AutoDisposeNotifieroderFamilyNotifiermehr; ein Family-Argument gelangt über den Konstruktor in den Notifier. Refist nicht mehr generisch, und mitref.mountedprüfen Sie nach einemawait, ob der Provider noch lebt.- Automatische Wiederholung: Fehlgeschlagene Provider werden standardmäßig mit wachsenden Abständen erneut versucht. Wenn Sie das nicht möchten, schalten Sie es mit
ProviderScope(retry: (retryCount, error) => null, ...)ab. - Unsichtbare Widgets pausieren: Widgets, die nicht auf dem Bildschirm sind, hören vorübergehend nicht mehr auf Provider.
- Alle Provider filtern Änderungen mit
==. Ist der neue Wert gleich dem alten, werden die Zuhörer nicht benachrichtigt. AsyncValueist sealed, undvalueersetztvalueOrNull.
Wann sollten Sie Riverpod verwenden?
| Situation | Provider | Riverpod |
|---|---|---|
| Einfache Apps | ✅ | ✅ |
| Große Projekte | ⚠️ | ✅ |
| Viele voneinander abhängige Provider | ❌ | ✅ |
| Hoher Testbedarf | ⚠️ | ✅ |
| Lesen außerhalb des Widget-Baums | ❌ | ✅ (mit ProviderContainer) |
| Neue Projekte | ⚠️ | ✅ |
Faustregel: Wenn Sie ein neues Projekt beginnen oder viele voneinander abhängige Zustände haben, ist Riverpod eine gute Wahl.
Provider im Vergleich zu Riverpod
| Merkmal | Provider | Riverpod |
|---|---|---|
| Lesen | Über den BuildContext |
Über ein ref (WidgetRef, Ref, ProviderContainer) |
| Fehlender Provider | Laufzeitfehler | Kompilierfehler |
| Zwei Provider desselben Typs | Unzuverlässig | Kein Problem |
| Provider kombinieren | Mit ProxyProvider, mühsam |
Mit ref.watch |
| Automatisches Aufräumen | Nein | autoDispose |
| Parametrisierte Provider | Nein | family |
Vor beiden gilt: Kurzlebige Daten, die nur zu einem Bildschirm gehören, sind im State des Widgets gut aufgehoben; wie initState und dispose das übernehmen, zeigt der Beitrag zum Lebenszyklus.
⚠️ Wichtiger Hinweis: Riverpod ist ein externes Paket und funktioniert nicht in DartPad. Um die Beispiele auf Ihrem Rechner auszuführen, folgen Sie den Installationsschritten unten.
Installation
Am einfachsten fügen Sie das Paket im Terminal hinzu; der Befehl trägt die aktuelle Version von pub.dev in Ihre pubspec.yaml ein:
flutter pub add flutter_riverpodWenn Sie es von Hand eintragen möchten, prüfen Sie die aktuelle Version auf der pub.dev-Seite von flutter_riverpod. Bei der Aktualisierung dieses Beitrags war 3.4.3 die neueste Version:
dependencies:
flutter:
sdk: flutter
flutter_riverpod: ^3.4.3Wer mit Codegenerierung (der Annotation @riverpod) arbeiten möchte, findet dafür die Pakete riverpod_annotation und riverpod_generator. Die Beispiele in diesem Beitrag verwenden die Schreibweise ohne Codegenerierung.
Grundeinrichtung
Umschließen Sie Ihre App mit ProviderScope:
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}Provider-Typen
1. Provider (unveränderliche oder abgeleitete Werte)
// Einfacher Wert
final greetingProvider = Provider<String>((ref) {
return 'Hallo Flutter!';
});
// Verwendung
class MyWidget extends ConsumerWidget {
const MyWidget({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final greeting = ref.watch(greetingProvider);
return Text(greeting);
}
}2. NotifierProvider (veränderlicher Zustand)
In Riverpod 3 ist Notifier der empfohlene Weg für veränderlichen Zustand. Die Methode build() liefert den Anfangswert, die Operationen, die den Zustand ändern, sind Methoden der Klasse:
class Counter extends Notifier<int> {
@override
int build() => 0; // Anfangswert
void increment() => state++;
void decrement() => state--;
void reset() => state = 0;
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
// Verwendung
class CounterPage extends ConsumerWidget {
const CounterPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('$count')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}ref.watch(counterProvider) liefert den Wert (int), ref.read(counterProvider.notifier) das Counter-Objekt. Ändern Sie den Zustand nicht von außen mit ref.read(counterProvider.notifier).state = 5: Der Setter von state ist mit @protected markiert, und der Analyzer warnt davor. Gehen Sie immer über eine Methode; so liegt alles, was den Zustand ändert, in einer Klasse.
3. FutureProvider (asynchrone Daten, nur lesend)
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
return User.fromJson(jsonDecode(response.body));
});
// Verwendung
class UserPage extends ConsumerWidget {
const UserPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final userAsync = ref.watch(userProvider);
return userAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Fehler: $error'),
data: (user) => Text('Hallo ${user.name}'),
);
}
}Da AsyncValue in Riverpod 3 eine sealed class ist, können Sie statt when auch einen switch-Ausdruck verwenden:
return switch (userAsync) {
AsyncData(:final value) => Text('Hallo ${value.name}'),
AsyncError(:final error) => Text('Fehler: $error'),
_ => const CircularProgressIndicator(),
};4. AsyncNotifierProvider (asynchrone Daten, die sich auch ändern lassen)
Wenn Sie Daten vom Server laden und anschließend damit arbeiten, verwenden Sie einen AsyncNotifier. Hier liefert build() ein Future:
class ProfileNotifier extends AsyncNotifier<User> {
@override
Future<User> build() => fetchUser();
Future<void> rename(String newName) async {
final current = await future;
state = const AsyncLoading();
state = await AsyncValue.guard(
() => saveUser(current.copyWith(name: newName)),
);
}
}
final profileProvider =
AsyncNotifierProvider<ProfileNotifier, User>(ProfileNotifier.new);
// Verwendung: ref.watch(profileProvider) liefert ein AsyncValue<User>
// ref.read(profileProvider.notifier).rename('Anna');AsyncValue.guard setzt den Zustand auf AsyncError, wenn die Operation eine Ausnahme wirft, und auf AsyncData, wenn sie gelingt; ein try/catch ist nicht nötig.
5. StreamProvider (Daten aus einem Stream)
final messagesProvider = StreamProvider<List<Message>>((ref) {
return FirebaseFirestore.instance
.collection('messages')
.snapshots()
.map((snapshot) => snapshot.docs.map((doc) => Message.fromDoc(doc)).toList());
});
// Verwendung
class MessagesPage extends ConsumerWidget {
const MessagesPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final messagesAsync = ref.watch(messagesProvider);
return messagesAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Fehler: $error'),
data: (messages) => ListView.builder(
itemCount: messages.length,
itemBuilder: (context, index) => ListTile(
title: Text(messages[index].text),
),
),
);
}
}ConsumerWidget im Vergleich zu Consumer
ConsumerWidget (das ganze Widget)
class MyPage extends ConsumerWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}Consumer (nur ein Ausschnitt)
class MyPage extends StatelessWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Seite')), // Wird nicht neu aufgebaut
body: Consumer(
builder: (context, ref, child) {
final count = ref.watch(counterProvider);
return Text('$count'); // Nur das wird neu aufgebaut
},
),
);
}
}Für Bildschirme, die initState, dispose oder einen TextEditingController brauchen, gibt es ConsumerStatefulWidget und ConsumerState; dort steht ref als Feld der State-Klasse bereit.
ref.watch im Vergleich zu ref.read
// ✅ In build watch verwenden (hört auf Änderungen)
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
// ✅ In Ereignisbehandlern read verwenden (einmaliges Lesen)
onPressed: () {
ref.read(counterProvider.notifier).increment();
}
// ❌ read nicht in build verwenden
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.read(counterProvider); // Wird bei Änderungen nicht aktualisiert
return Text('$count');
}Praxisbeispiel: eine To-do-App
// Todo-Modell
class Todo {
final String id;
final String title;
final bool isCompleted;
const Todo({required this.id, required this.title, this.isCompleted = false});
Todo copyWith({String? title, bool? isCompleted}) {
return Todo(
id: id,
title: title ?? this.title,
isCompleted: isCompleted ?? this.isCompleted,
);
}
}
// Aufgabenliste
class TodoList extends Notifier<List<Todo>> {
@override
List<Todo> build() => [];
void add(String title) {
state = [
...state,
Todo(id: DateTime.now().microsecondsSinceEpoch.toString(), title: title),
];
}
void toggle(String id) {
state = [
for (final todo in state)
if (todo.id == id) todo.copyWith(isCompleted: !todo.isCompleted) else todo,
];
}
void remove(String id) {
state = state.where((todo) => todo.id != id).toList();
}
}
final todoListProvider = NotifierProvider<TodoList, List<Todo>>(TodoList.new);
// Filter
enum TodoFilter { all, completed, uncompleted }
class TodoFilterNotifier extends Notifier<TodoFilter> {
@override
TodoFilter build() => TodoFilter.all;
void change(TodoFilter filter) => state = filter;
}
final todoFilterProvider =
NotifierProvider<TodoFilterNotifier, TodoFilter>(TodoFilterNotifier.new);
// Gefilterte Liste: aus zwei Providern abgeleitet
final filteredTodosProvider = Provider<List<Todo>>((ref) {
final todos = ref.watch(todoListProvider);
final filter = ref.watch(todoFilterProvider);
return switch (filter) {
TodoFilter.completed => todos.where((todo) => todo.isCompleted).toList(),
TodoFilter.uncompleted => todos.where((todo) => !todo.isCompleted).toList(),
TodoFilter.all => todos,
};
});
// UI
class TodoPage extends ConsumerWidget {
const TodoPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final todos = ref.watch(filteredTodosProvider);
return Scaffold(
appBar: AppBar(
title: const Text('Aufgaben'),
actions: [
PopupMenuButton<TodoFilter>(
onSelected: (filter) {
ref.read(todoFilterProvider.notifier).change(filter);
},
itemBuilder: (context) => const [
PopupMenuItem(value: TodoFilter.all, child: Text('Alle')),
PopupMenuItem(value: TodoFilter.completed, child: Text('Erledigt')),
PopupMenuItem(value: TodoFilter.uncompleted, child: Text('Offen')),
],
),
],
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
final todo = todos[index];
return ListTile(
leading: Checkbox(
value: todo.isCompleted,
onChanged: (_) => ref.read(todoListProvider.notifier).toggle(todo.id),
),
title: Text(
todo.title,
style: TextStyle(
decoration: todo.isCompleted ? TextDecoration.lineThrough : null,
),
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => ref.read(todoListProvider.notifier).remove(todo.id),
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () => _showAddDialog(context, ref),
child: const Icon(Icons.add),
),
);
}
void _showAddDialog(BuildContext context, WidgetRef ref) {
final controller = TextEditingController();
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Neue Aufgabe'),
content: TextField(
controller: controller,
decoration: const InputDecoration(hintText: 'Name der Aufgabe'),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Abbrechen'),
),
ElevatedButton(
onPressed: () {
if (controller.text.isNotEmpty) {
ref.read(todoListProvider.notifier).add(controller.text);
Navigator.pop(context);
}
},
child: const Text('Hinzufügen'),
),
],
),
);
}
}In Beispielen für Riverpod 2 sehen Sie für den Filter meist einen StateProvider. In Riverpod 3 erledigt ein kleiner Notifier wie oben dieselbe Aufgabe; weil jede Änderung über die Methode change läuft, sieht man beim Lesen sofort, wo der Filter geändert wird.
Provider kombinieren
// Nutzer-Provider
final userProvider = FutureProvider<User>((ref) async {
return fetchUser();
});
// Bestellungen des Nutzers (abhängig von userProvider)
final userOrdersProvider = FutureProvider<List<Order>>((ref) async {
final user = await ref.watch(userProvider.future);
return fetchOrders(user.id);
});
// Gesamtbetrag der Bestellungen
final totalOrderAmountProvider = Provider<double>((ref) {
final orders = ref.watch(userOrdersProvider).value ?? [];
return orders.fold(0.0, (sum, order) => sum + order.amount);
});In Riverpod 3 liefert AsyncValue.value null, solange keine Daten vorliegen (beim Laden oder bei einem Fehler); es ersetzt valueOrNull aus Riverpod 2.
Der Modifier family
Parametrisierte Provider erstellen:
// Provider für Produktdetails
final productProvider = FutureProvider.family<Product, String>((ref, productId) async {
final response = await http.get(
Uri.parse('https://api.example.com/products/$productId'),
);
return Product.fromJson(jsonDecode(response.body));
});
// Verwendung
class ProductPage extends ConsumerWidget {
final String productId;
const ProductPage({super.key, required this.productId});
@override
Widget build(BuildContext context, WidgetRef ref) {
final productAsync = ref.watch(productProvider(productId));
return productAsync.when(
loading: () => const CircularProgressIndicator(),
error: (error, stack) => Text('Fehler: $error'),
data: (product) => Text(product.name),
);
}
}Bei einer Family mit Notifier kommt das Argument in Riverpod 3 über den Konstruktor:
class ProductQuantity extends Notifier<int> {
ProductQuantity(this.productId);
final String productId;
@override
int build() => 1;
void increment() => state++;
}
final productQuantityProvider =
NotifierProvider.family<ProductQuantity, int, String>(ProductQuantity.new);
// Verwendung
// ref.watch(productQuantityProvider('produkt-42'));
// ref.read(productQuantityProvider('produkt-42').notifier).increment();Family-Argumente werden mit == verglichen. Übergeben Sie eine Liste, die bei jedem build neu entsteht, oder ein Objekt ohne sinnvolles ==, entsteht jedes Mal ein neuer Provider; verwenden Sie als Argument einen String, einen int oder Wertobjekte mit korrekt definiertem ==.
Der Modifier autoDispose
Mit .autoDispose wird der Zustand verworfen, sobald niemand mehr zuhört. Für eine Anfrage, die bei jedem Tastendruck neu läuft (etwa ein Suchfeld), sieht das Debounce-Muster aus der Riverpod-Dokumentation so aus:
final searchProvider =
FutureProvider.autoDispose.family<List<Product>, String>((ref, query) async {
// Tippt die Person weiter, wird dieser Provider verworfen
var didDispose = false;
ref.onDispose(() => didDispose = true);
await Future<void>.delayed(const Duration(milliseconds: 500));
if (didDispose) {
throw Exception('Abgebrochen');
}
return searchProducts(query);
});In Riverpod 3 geht dieselbe Prüfung auch mit if (!ref.mounted) ...; ref.mounted sagt Ihnen nach einem await, ob der Provider noch lebt.
Nebeneffekte mit ref.listen
class MyPage extends ConsumerWidget {
const MyPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Nebeneffekt ausführen, wenn sich der Zustand ändert
ref.listen<int>(counterProvider, (previous, next) {
if (next == 10) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Sie haben 10 erreicht!')),
);
}
});
return Text('${ref.watch(counterProvider)}');
}
}Aktualisieren mit ref.invalidate
// Provider erneut ausführen
ElevatedButton(
onPressed: () {
ref.invalidate(userProvider);
},
child: const Text('Aktualisieren'),
)Best Practices
1. Provider global definieren
// ✅ Auf oberster Ebene der Datei
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
// ❌ Nicht innerhalb eines Widgets: jeder build erzeugt einen neuen Provider
class MyWidget extends ConsumerWidget {
final counterProvider = NotifierProvider<Counter, int>(Counter.new); // Falsch!
}2. Kleine, fokussierte Provider erstellen
// ✅ Richtig: jeder Provider erledigt genau eine Aufgabe
final userProvider = FutureProvider<User>(...);
final userOrdersProvider = FutureProvider<List<Order>>(...);
final userBalanceProvider = Provider<double>(...);
// ❌ Falsch: ein Provider, der zu viel erledigt
final everythingProvider = FutureProvider<Everything>(...);3. Unnötige Rebuilds mit select vermeiden
// ✅ Baut nur neu auf, wenn sich die Anzahl der Aufgaben ändert
final count = ref.watch(todoListProvider.select((todos) => todos.length));
// ❌ Baut bei jeder Änderung der Liste neu auf
final todos = ref.watch(todoListProvider);Älterer Code: StateProvider und StateNotifier
Wenn Sie ein Projekt von Riverpod 2 auf 3 aktualisieren, kompilieren Dateien mit StateProvider und StateNotifierProvider nicht mehr, weil diese Klassen aus dem Hauptimport entfernt wurden. Sie haben zwei Möglichkeiten.
Schnelle Lösung: Ergänzen Sie in den betroffenen Dateien den Legacy-Import. Der Code läuft unverändert weiter:
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_riverpod/legacy.dart'; // StateProvider, StateNotifierProvider, ChangeNotifierProviderDauerhafte Lösung: Stellen Sie nach und nach auf Notifier um. Die Umstellung ist größtenteils mechanisch:
// Riverpod 2: StateNotifier
class CounterNotifier extends StateNotifier<int> {
CounterNotifier() : super(0);
void increment() => state++;
}
final counterProvider =
StateNotifierProvider<CounterNotifier, int>((ref) => CounterNotifier());
// Riverpod 3: Notifier
class Counter extends Notifier<int> {
@override
int build() => 0; // build() statt super(0)
void increment() => state++;
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);Die Aufrufe im Widget, ref.watch(counterProvider) und ref.read(counterProvider.notifier).increment(), bleiben gleich. Wo Sie einen StateProvider verwendet haben, schreiben Sie statt ref.read(provider.notifier).state = x einen kleinen Notifier mit einer Methode, wie im Filterbeispiel oben. Eine Schritt-für-Schritt-Anleitung finden Sie im Migrationsleitfaden zu Riverpod 3.
Zusammenfassung
Riverpod: ein State-Management-Paket, das die Grenzen von Provider beseitigen sollProviderScope: der Geltungsbereich, der die Anwendung umschließt und den Zustand hältref: das, was Sie zum Lesen von Providern brauchen (WidgetRef,Ref,ProviderContainer)NotifierProvider: für veränderlichen synchronen ZustandAsyncNotifierProvider: für asynchronen Zustand, den Sie laden und ändernFutureProvider/StreamProvider: für asynchrone Daten, die nur gelesen werdenref.watch: in build verwenden (hört mit)ref.read: in Ereignissen verwenden (hört nicht mit)family: parametrisierter ProviderautoDispose: automatisches Aufräumenlegacy.dart: für älteren Code mitStateProviderundStateNotifierProvider
Riverpod ist eine starke Lösung für State Management in Flutter-Projekten mit vielen voneinander abhängigen Zuständen. Für etwas so Schmales wie die Anzeige eines einzelnen asynchronen Ergebnisses brauchen Sie womöglich gar kein Paket – dafür genügt FutureBuilder.
Häufig gestellte Fragen
Sollte ich von Provider zu Riverpod wechseln?
Eine funktionierende Provider-Einrichtung nur der Neuheit wegen umzubauen, lohnt sich nicht. Beginnen Sie dagegen ein neues Projekt, oder ärgern Sie sich regelmäßig über ProviderNotFoundException und das umständliche Kombinieren von Providern, zahlt sich der Wechsel aus – und er lässt sich schrittweise vollziehen.
Läuft mein Riverpod-2-Code unter Riverpod 3?
Größtenteils ja. Dateien mit StateProvider, StateNotifierProvider oder ChangeNotifierProvider brauchen den Import package:flutter_riverpod/legacy.dart. Klassen wie AutoDisposeNotifier und FamilyNotifier wurden entfernt; sie müssen Sie in Notifier umwandeln und das Family-Argument in den Konstruktor verlegen.
Worin unterscheiden sich ref.watch und ref.read?
ref.watch liest den Wert und baut das Widget neu auf, sobald sich der Provider ändert; es gehört daher in build. ref.read holt nur den aktuellen Wert, ohne sich anzumelden, und ist in Ereignisbehandlern wie Button-Callbacks die richtige Wahl.
Wann ConsumerWidget, wann Consumer?
Hängt das gesamte Widget an einem Provider, ist ConsumerWidget die schlankere Schreibweise. Ändert sich nur ein kleiner Ausschnitt des Bildschirms, umschließen Sie genau diesen mit einem Consumer, damit der neu aufgebaute Teilbaum klein bleibt.
Verwandte Artikel
Flutter: State Management mit Provider
State Management mit dem Paket Provider in Flutter: ChangeNotifier, Consumer, MultiProvider und Best Practices.
Flutter-Lebenszyklus: StatefulWidget und AppLifecycleState
Die Lebenszyklus-Methoden von StatefulWidget und die Verwaltung des App-Lebenszyklus in Flutter verstehen: initState, dispose, didUpdateWidget und setState.
Navigation und Datenübergabe zwischen Seiten in Flutter
Seitenwechsel, Datenübergabe und Back-Stack mit dem Flutter-Navigator: push, pop, pushReplacement und das aktuelle PopScope für den Zurück-Button.