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

Flutter: State Management mit Provider

Ahmet Balaman
FlutterProviderState ManagementChangeNotifierConsumer

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

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.

Kommentare