Flutter: State Management mit Provider
Zuletzt aktualisiert:
5 Min. Lesezeit

Provider gehört zu den beliebtesten Paketen für State Management in Flutter; auch der offizielle Leitfaden zu einfachem State Management des Flutter-Teams erklärt seine Beispiele mit diesem Paket. Es dient dazu, Daten im Widget-Baum zu teilen und auf Änderungen zu reagieren.
Was ist State und warum muss man ihn verwalten?
State ist der Datenzustand, den Ihre Anwendung in einem bestimmten Moment hat. Zum Beispiel:
- Ob die Nutzerin angemeldet ist
- Die Anzahl der Artikel im Warenkorb
- Die Theme-Einstellung (hell/dunkel)
- Die Werte in Formularfeldern
- Daten von APIs
Ohne State Management wird es sehr schwierig, diese Daten zwischen Widgets zu teilen und Aktualisierungen abzubilden.
Die Grenzen von setState
Für einfaches State Management nutzt Flutter setState; wo setState im StatefulWidget sitzt und wann es läuft, beschreibt der Beitrag zum Lebenszyklus. Wächst Ihre App jedoch, stoßen Sie auf ernste Probleme:
1. Das Problem des Prop Drilling
// You have to pass data 5 levels down
GrandParent(
child: Parent(
child: Child(
child: GrandChild(
child: GreatGrandChild(
userData: userData, // Must be passed all the way!
),
),
),
),
)2. Unnötige Neuaufbauten
Bei setState wird das gesamte Widget neu aufgebaut – nicht nur der geänderte Teil.
3. Verlust des Zustands
Beim Navigieren durch den Widget-Baum kann der Zustand verloren gehen. Wechseln Sie die Seite und kehren zurück, sind die Daten zurückgesetzt.
4. Schlechte Testbarkeit
Sind Oberfläche und Geschäftslogik verflochten, wird es schwer, Unit-Tests zu schreiben.
5. Codewiederholung
Um dieselben Daten an mehreren Stellen zu nutzen, müssen Sie ständig Parameter weiterreichen.
Wie Provider diese Probleme löst
✅ Zentrale Zustandsverwaltung
Die Daten werden an einer Stelle verwaltet und sind von überall erreichbar.
✅ Effiziente Neuaufbauten
Nur die tatsächlich betroffenen Widgets werden neu aufgebaut.
✅ Trennung der Zuständigkeiten
Geschäftslogik und Oberfläche sind voneinander getrennt.
✅ Gute Testbarkeit
Sie können Provider leicht durch Mocks ersetzen und Tests schreiben.
✅ Kein Prop Drilling
Widgets, die Daten brauchen, lesen sie direkt aus dem Provider.
Wann sollten Sie Provider verwenden?
| Situation | setState | Provider |
|---|---|---|
| Einfacher Zähler in einem Widget | ✅ | ❌ |
| Formularvalidierung | ✅ | ❌ |
| Benutzerdaten über mehrere Seiten | ❌ | ✅ |
| Verwaltung des Warenkorbs | ❌ | ✅ |
| Einstellungen zu Theme und Sprache | ❌ | ✅ |
| Daten von APIs | ❌ | ✅ |
| Komplexe Formularzustände | ❌ | ✅ |
Faustregel: Wird ein Zustand von mehreren Widgets oder Seiten genutzt, sollten Sie Provider in Betracht ziehen.
Warum Provider?
| Merkmal | setState | Provider |
|---|---|---|
| Geltungsbereich | Einzelnes Widget | Gesamter Widget-Baum |
| Komplexität | Einfach | Mittel |
| Skalierbarkeit | Schwierig | Einfach |
| Testbarkeit | Schwierig | Einfach |
| Prop Drilling | Erforderlich | Nicht nötig |
Wenn Sie die Abhängigkeit vom BuildContext und die fehlende Sicherheit zur Übersetzungszeit stört, ist Riverpod der nächste Schritt derselben Idee: Provider werden global definiert, brauchen keinen Context, und ein Zugriff mit falschem Typ scheitert schon beim Übersetzen.
⚠️ Wichtiger Hinweis: Provider ist ein externes Paket und funktioniert nicht in DartPad. Um die Beispiele auf Ihrem Rechner auszuführen, folgen Sie den Installationsschritten unten.
Installation
Die aktuelle Version finden Sie auf der pub.dev-Seite des Pakets provider; ergänzen Sie Ihre Datei pubspec.yaml:
dependencies:
flutter:
sdk: flutter
provider: ^6.1.1Führen Sie anschließend im Terminal aus:
flutter pub getDie zentralen Konzepte
1. ChangeNotifier
Die Klasse, die den Zustand hält und über Änderungen informiert; die API-Dokumentation der Klasse ChangeNotifier, die zu Flutter selbst gehört, beschreibt das Hinzufügen von Listenern und das Verhalten von notifyListeners:
import 'package:flutter/foundation.dart';
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners(); // Notify listeners
}
void decrement() {
_count--;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}2. ChangeNotifierProvider
Den Provider in den Widget-Baum einhängen:
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MyApp(),
),
);
}3. Consumer
Auf den Zustand hören und die Oberfläche aktualisieren:
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 48),
);
},
)4. context.read und context.watch
// Read (doesn't listen to changes) - Use in buttons
context.read<CounterProvider>().increment();
// Watch (listens to changes) - Use in build method
final count = context.watch<CounterProvider>().count;Vollständiges Beispiel: eine Zähler-App
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
// 1. State class
class CounterProvider extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
_count--;
notifyListeners();
}
}
// 2. Main
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterProvider(),
child: MaterialApp(
home: CounterPage(),
),
),
);
}
// 3. UI
class CounterPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider Counter')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Counter Value:'),
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: TextStyle(fontSize: 72, fontWeight: FontWeight.bold),
);
},
),
SizedBox(height: 32),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FloatingActionButton(
onPressed: () => context.read<CounterProvider>().decrement(),
child: Icon(Icons.remove),
),
SizedBox(width: 16),
FloatingActionButton(
onPressed: () => context.read<CounterProvider>().increment(),
child: Icon(Icons.add),
),
],
),
],
),
),
);
}
}MultiProvider
Mehrere Provider gemeinsam verwenden:
void main() {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => CounterProvider()),
ChangeNotifierProvider(create: (_) => ThemeProvider()),
ChangeNotifierProvider(create: (_) => UserProvider()),
],
child: MyApp(),
),
);
}Praxisbeispiel: Theme umschalten
// Theme Provider
class ThemeProvider extends ChangeNotifier {
ThemeMode _themeMode = ThemeMode.light;
ThemeMode get themeMode => _themeMode;
bool get isDarkMode => _themeMode == ThemeMode.dark;
void toggleTheme() {
_themeMode = isDarkMode ? ThemeMode.light : ThemeMode.dark;
notifyListeners();
}
}
// Main
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => ThemeProvider(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ThemeProvider>(
builder: (context, themeProvider, child) {
return MaterialApp(
themeMode: themeProvider.themeMode,
theme: ThemeData.light(),
darkTheme: ThemeData.dark(),
home: HomePage(),
);
},
);
}
}
class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final themeProvider = context.watch<ThemeProvider>();
return Scaffold(
appBar: AppBar(title: Text('Theme Settings')),
body: Center(
child: SwitchListTile(
title: Text('Dark Mode'),
value: themeProvider.isDarkMode,
onChanged: (_) => themeProvider.toggleTheme(),
),
),
);
}
}In dieser Form lebt die Einstellung nur im Speicher, nach einem Neustart ist die App wieder hell. Wie derselbe ChangeNotifier den Wert dauerhaft speichert und beim Start wieder einliest, zeigt Schritt für Schritt der Beitrag lokale Datenspeicherung mit SharedPreferences.
Praxisbeispiel: Warenkorb
// Product model
class Product {
final String id;
final String name;
final double price;
Product({required this.id, required this.name, required this.price});
}
// Cart Provider
class CartProvider extends ChangeNotifier {
final List<Product> _items = [];
List<Product> get items => List.unmodifiable(_items);
int get itemCount => _items.length;
double get totalPrice => _items.fold(0, (sum, item) => sum + item.price);
void addItem(Product product) {
_items.add(product);
notifyListeners();
}
void removeItem(String productId) {
_items.removeWhere((item) => item.id == productId);
notifyListeners();
}
void clearCart() {
_items.clear();
notifyListeners();
}
}
// Usage
class ProductCard extends StatelessWidget {
final Product product;
ProductCard({required this.product});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(
title: Text(product.name),
subtitle: Text('\$${product.price}'),
trailing: IconButton(
icon: Icon(Icons.add_shopping_cart),
onPressed: () {
context.read<CartProvider>().addItem(product);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('${product.name} added to cart')),
);
},
),
),
);
}
}
// Cart icon (in AppBar)
class CartIcon extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Stack(
children: [
IconButton(
icon: Icon(Icons.shopping_cart),
onPressed: () {
// Navigate to cart page
},
),
Positioned(
right: 0,
top: 0,
child: Consumer<CartProvider>(
builder: (context, cart, child) {
return cart.itemCount > 0
? CircleAvatar(
radius: 10,
backgroundColor: Colors.red,
child: Text(
'${cart.itemCount}',
style: TextStyle(fontSize: 12, color: Colors.white),
),
)
: SizedBox.shrink();
},
),
),
],
);
}
}Optimierung mit Selector
Nur auf bestimmte Änderungen hören:
// Listens to all changes (inefficient)
Consumer<CartProvider>(
builder: (context, cart, child) {
return Text('${cart.itemCount} items');
},
)
// Rebuilds only when itemCount changes (efficient)
Selector<CartProvider, int>(
selector: (context, cart) => cart.itemCount,
builder: (context, itemCount, child) {
return Text('$itemCount items');
},
)Arten von Providern
| Typ | Verwendung |
|---|---|
Provider |
Unveränderliche Werte |
ChangeNotifierProvider |
Veränderlicher Zustand (am häufigsten) |
FutureProvider |
Asynchrone Daten |
StreamProvider |
Daten aus einem Stream |
ProxyProvider |
Voneinander abhängige Provider |
Beispiel für FutureProvider
FutureProvider<List<Product>>(
create: (_) => fetchProducts(),
initialData: [],
child: ProductList(),
)Best Practices
1. Definieren Sie den Provider so weit oben wie möglich
// ✅ Correct - In main
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => MyProvider(),
child: MyApp(),
),
);
}
// ❌ Wrong - Deep in tree
class SomePage extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) => MyProvider(), // New instance on every build!
child: ...,
);
}
}2. context.read im Vergleich zu context.watch
// ✅ Use watch in build method
Widget build(BuildContext context) {
final count = context.watch<CounterProvider>().count;
return Text('$count');
}
// ✅ Use read in event handlers
onPressed: () {
context.read<CounterProvider>().increment();
}
// ❌ Don't use read in build method
Widget build(BuildContext context) {
final count = context.read<CounterProvider>().count; // Won't update
return Text('$count');
}3. Halten Sie den Consumer so eng wie möglich
// ✅ Correct - Only necessary part rebuilds
Scaffold(
appBar: AppBar(title: Text('Page')),
body: Consumer<CounterProvider>(
builder: (context, counter, child) {
return Text('${counter.count}');
},
),
)
// ❌ Wrong - Entire page rebuilds
Consumer<CounterProvider>(
builder: (context, counter, child) {
return Scaffold(
appBar: AppBar(title: Text('Page')),
body: Text('${counter.count}'),
);
},
)Zusammenfassung
Provider: die empfohlene Lösung für State Management in FlutterChangeNotifier: die Klasse, die den Zustand hält und über Änderungen informiertnotifyListeners(): wird aufgerufen, um die Oberfläche zu aktualisierenConsumer: reagiert auf Zustandsänderungencontext.watch: in build verwenden (hört mit)context.read: in Ereignissen verwenden (hört nicht mit)MultiProvider: mehrere ProviderSelector: Optimierung der Performance
Provider ist eine leistungsfähige und flexible Lösung für State Management und eignet sich für Flutter-Projekte jeder Größe. Gehört ein Datum nur einem einzigen Bildschirm, bleibt setState der einfachste Weg; für das einmalige Anzeigen eines asynchronen Ergebnisses genügt oft FutureBuilder.
Häufig gestellte Fragen
Provider oder setState?
Lebt der Zustand nur in einem einzigen Widget und liest ihn niemand von außen, ist setState der einfachste und schnellste Weg. Sobald mehrere Bildschirme dieselben Daten lesen oder ändern, lohnt der Wechsel zu Provider – der eigentliche Gewinn ist der Verzicht auf Prop Drilling.
Worin unterscheiden sich context.read und context.watch?
context.watch liest den Wert und meldet das Widget für Änderungen an; es gehört daher in build. context.read liest einmalig, ohne sich anzumelden, und ist in Ereignisbehandlern wie onPressed die richtige Wahl.
Ich rufe notifyListeners() auf, aber die Oberfläche ändert sich nicht – warum?
Meist liegt es an einem von zwei Dingen: Das lesende Widget verwendet context.read und meldet sich damit nie an, oder Sie haben eine Liste an Ort und Stelle verändert und dieselbe Referenz zurückgegeben. Ändern Sie den Zustand im ChangeNotifier, rufen Sie notifyListeners() auf, und lesen Sie auf der anderen Seite mit context.watch oder einem Consumer.
Verwandte Artikel
Flutter: Modernes State Management mit Riverpod 3
State Management in Flutter mit Riverpod 3: Notifier, AsyncNotifier, ref.watch und ref.read, family, autoDispose und Hinweise zum Umstieg von StateNotifier.
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.