Flutter: State Management mit Provider
Provider gehört zu den beliebtesten und von Google empfohlenen Paketen für State Management in Flutter. 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. 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 |
⚠️ 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
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:
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(),
),
),
);
}
}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
final userProvider = FutureProvider<User>((ref) async {
final response = await http.get(Uri.parse('https://api.example.com/user'));
return User.fromJson(jsonDecode(response.body));
});
// Usage
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 Flutter
- ChangeNotifier: Die Klasse, die den Zustand hält und über Änderungen informiert
- notifyListeners(): Wird aufgerufen, um die Oberfläche zu aktualisieren
- Consumer: Reagiert auf Zustandsänderungen
- context.watch: In build verwenden (hört mit)
- context.read: In Ereignissen verwenden (hört nicht mit)
- MultiProvider: Mehrere Provider
- Selector: 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.