İçeriğe geç / Skip to content / Zum Inhalt

Flutter: State Management mit Provider

Ahmet Balaman

Zuletzt aktualisiert:

5 Min. Lesezeit

FlutterProviderState ManagementChangeNotifierConsumer
Flutter: State Management mit Provider

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

Ruft ChangeNotifier notifyListeners auf, wird nur der Consumer neu gebaut, die übrigen Widgets im Baum nicht

✅ 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.1

Führen Sie anschließend im Terminal aus:

flutter pub get

Die 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 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. 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.

Kommentare