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

Flutter: Modernes State Management mit Riverpod

Ahmet Balaman
FlutterRiverpodState ManagementStateNotifierProvider

Riverpod ist eine moderne Lösung für State Management von Remi Rousselet, dem Entwickler von Provider. Sie räumt sämtliche Einschränkungen von Provider aus und bietet Sicherheit zur Übersetzungszeit, gute Testbarkeit und viel Flexibilität.

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

Um diese Komplexität zu beherrschen, ist eine leistungsfähige Lösung für State Management unverzichtbar.

Die Grenzen von Provider

Provider ist ein großartiges Werkzeug, hat aber einige Einschränkungen:

1. Abhängigkeit vom BuildContext

// You always need context to read Provider
final user = Provider.of<UserProvider>(context);

// You can't access it in places without context (utility functions, services)

2. Fehler erst zur Laufzeit

// If Provider is not found, the app throws error AT RUNTIME
// Cannot be caught at compile time
final data = context.read<SomeProvider>(); // ProviderNotFoundException!

3. Provider lassen sich schwer kombinieren

Sobald ein Provider von einem anderen abhängt, wird es kompliziert.

4. Probleme beim Hot Reload

Provider aktualisieren sich beim Hot Reload manchmal nicht korrekt.

5. Zugriff auf globalen Zustand

Auf den Zustand von außerhalb des Widget-Baums zuzugreifen ist umständlich.

Wie Riverpod diese Probleme löst

✅ Kein BuildContext nötig

// Riverpod: Access from anywhere without context
final user = ref.watch(userProvider);

// Works in service classes, tests, everywhere

✅ Sicherheit zur Übersetzungszeit

// If provider is not found, code WON'T COMPILE
// You catch errors before running
final data = ref.watch(someProvider); // Compile-time check!

✅ Provider lassen sich leicht kombinieren

// A provider can easily watch other providers
final userOrdersProvider = FutureProvider((ref) async {
  final user = await ref.watch(userProvider.future);
  return fetchOrders(user.id);
});

✅ Hervorragende Testbarkeit

// You can easily override providers
ProviderScope(
  overrides: [
    userProvider.overrideWithValue(mockUser),
  ],
  child: MyApp(),
)

✅ Speicherverwaltung mit AutoDispose

Nicht genutzte Provider werden automatisch aufgeräumt.

Wann sollten Sie Riverpod verwenden?

Situation Provider Riverpod
Einfache Apps
Große Projekte im Unternehmensumfeld ⚠️
Viele voneinander abhängige Provider
Hoher Testbedarf ⚠️
Zugriff außerhalb des Widget-Baums
Neue Projekte ⚠️

Faustregel: Wenn Sie ein neues Projekt beginnen oder komplexe Anforderungen an den Zustand haben, greifen Sie zu Riverpod.

Provider im Vergleich zu Riverpod

Merkmal Provider Riverpod
BuildContext Erforderlich Nicht nötig
Sicherheit zur Übersetzungszeit Begrenzt Vollständig
Provider kombinieren Schwierig Einfach
Testbarkeit Mittel Hervorragend
Hot Reload Problematisch Reibungslos
Globaler Zugriff Über den BuildContext Von überall

⚠️ 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

Ergänzen Sie Ihre Datei pubspec.yaml:

dependencies:
  flutter:
    sdk: flutter
  flutter_riverpod: ^2.4.9

Führen Sie anschließend im Terminal aus:

flutter pub get

Grundeinrichtung

Umschließen Sie Ihre Anwendung mit ProviderScope:

import 'package:flutter_riverpod/flutter_riverpod.dart';

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

Arten von Providern

1. Provider (unveränderliche Werte)

// Simple value
final greetingProvider = Provider<String>((ref) {
  return 'Hello Flutter!';
});

// Usage
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final greeting = ref.watch(greetingProvider);
    return Text(greeting);
  }
}

2. StateProvider (einfacher Zustand)

// Counter state
final counterProvider = StateProvider<int>((ref) => 0);

// Usage
class CounterPage extends ConsumerWidget {
  @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).state++;
        },
        child: Icon(Icons.add),
      ),
    );
  }
}

3. StateNotifierProvider (komplexer Zustand)

// State class
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  
  void increment() => state++;
  void decrement() => state--;
  void reset() => state = 0;
}

// Provider definition
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});

// Usage
class CounterPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    
    return Column(
      children: [
        Text('$count'),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).increment(),
          child: Text('Increment'),
        ),
        ElevatedButton(
          onPressed: () => ref.read(counterProvider.notifier).decrement(),
          child: Text('Decrement'),
        ),
      ],
    );
  }
}

4. FutureProvider (asynchrone Daten)

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
class UserPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider);
    
    return userAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
      data: (user) => Text('Hello ${user.name}'),
    );
  }
}

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());
});

// Usage
class MessagesPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final messagesAsync = ref.watch(messagesProvider);
    
    return messagesAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
      data: (messages) => ListView.builder(
        itemCount: messages.length,
        itemBuilder: (context, index) => ListTile(
          title: Text(messages[index].text),
        ),
      ),
    );
  }
}

ConsumerWidget im Vergleich zu Consumer

ConsumerWidget (das gesamte Widget)

class MyPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final count = ref.watch(counterProvider);
    return Text('$count');
  }
}

Consumer (nur ein Ausschnitt)

class MyPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Page')), // Won't rebuild
      body: Consumer(
        builder: (context, ref, child) {
          final count = ref.watch(counterProvider);
          return Text('$count'); // Only this rebuilds
        },
      ),
    );
  }
}

ref.watch im Vergleich zu ref.read

// ✅ Use watch in build method (listens to changes)
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.watch(counterProvider);
  return Text('$count');
}

// ✅ Use read in event handlers (one-time read)
onPressed: () {
  ref.read(counterProvider.notifier).increment();
}

// ❌ Don't use read in build method
Widget build(BuildContext context, WidgetRef ref) {
  final count = ref.read(counterProvider); // Won't update
  return Text('$count');
}

Praxisbeispiel: eine To-do-App

// Todo model
class Todo {
  final String id;
  final String title;
  final bool isCompleted;
  
  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,
    );
  }
}

// Todo Notifier
class TodoNotifier extends StateNotifier<List<Todo>> {
  TodoNotifier() : super([]);
  
  void addTodo(String title) {
    state = [
      ...state,
      Todo(id: DateTime.now().toString(), title: title),
    ];
  }
  
  void toggleTodo(String id) {
    state = state.map((todo) {
      if (todo.id == id) {
        return todo.copyWith(isCompleted: !todo.isCompleted);
      }
      return todo;
    }).toList();
  }
  
  void removeTodo(String id) {
    state = state.where((todo) => todo.id != id).toList();
  }
}

// Provider
final todoProvider = StateNotifierProvider<TodoNotifier, List<Todo>>((ref) {
  return TodoNotifier();
});

// Filter provider
enum TodoFilter { all, completed, uncompleted }

final todoFilterProvider = StateProvider<TodoFilter>((ref) => TodoFilter.all);

// Filtered todo list
final filteredTodosProvider = Provider<List<Todo>>((ref) {
  final todos = ref.watch(todoProvider);
  final filter = ref.watch(todoFilterProvider);
  
  switch (filter) {
    case TodoFilter.completed:
      return todos.where((todo) => todo.isCompleted).toList();
    case TodoFilter.uncompleted:
      return todos.where((todo) => !todo.isCompleted).toList();
    case TodoFilter.all:
      return todos;
  }
});

// UI
class TodoPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final todos = ref.watch(filteredTodosProvider);
    
    return Scaffold(
      appBar: AppBar(
        title: Text('Tasks'),
        actions: [
          PopupMenuButton<TodoFilter>(
            onSelected: (filter) {
              ref.read(todoFilterProvider.notifier).state = filter;
            },
            itemBuilder: (context) => [
              PopupMenuItem(value: TodoFilter.all, child: Text('All')),
              PopupMenuItem(value: TodoFilter.completed, child: Text('Completed')),
              PopupMenuItem(value: TodoFilter.uncompleted, child: Text('Uncompleted')),
            ],
          ),
        ],
      ),
      body: ListView.builder(
        itemCount: todos.length,
        itemBuilder: (context, index) {
          final todo = todos[index];
          return ListTile(
            leading: Checkbox(
              value: todo.isCompleted,
              onChanged: (_) {
                ref.read(todoProvider.notifier).toggleTodo(todo.id);
              },
            ),
            title: Text(
              todo.title,
              style: TextStyle(
                decoration: todo.isCompleted 
                    ? TextDecoration.lineThrough 
                    : null,
              ),
            ),
            trailing: IconButton(
              icon: Icon(Icons.delete),
              onPressed: () {
                ref.read(todoProvider.notifier).removeTodo(todo.id);
              },
            ),
          );
        },
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _showAddDialog(context, ref),
        child: Icon(Icons.add),
      ),
    );
  }
  
  void _showAddDialog(BuildContext context, WidgetRef ref) {
    final controller = TextEditingController();
    
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text('New Task'),
        content: TextField(
          controller: controller,
          decoration: InputDecoration(hintText: 'Task name'),
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: Text('Cancel'),
          ),
          ElevatedButton(
            onPressed: () {
              if (controller.text.isNotEmpty) {
                ref.read(todoProvider.notifier).addTodo(controller.text);
                Navigator.pop(context);
              }
            },
            child: Text('Add'),
          ),
        ],
      ),
    );
  }
}

Provider kombinieren

// User provider
final userProvider = FutureProvider<User>((ref) async {
  return await fetchUser();
});

// User's orders (depends on userProvider)
final userOrdersProvider = FutureProvider<List<Order>>((ref) async {
  final user = await ref.watch(userProvider.future);
  return await fetchOrders(user.id);
});

// Total order amount
final totalOrderAmountProvider = Provider<double>((ref) {
  final ordersAsync = ref.watch(userOrdersProvider);
  
  return ordersAsync.when(
    loading: () => 0,
    error: (_, __) => 0,
    data: (orders) => orders.fold(0, (sum, order) => sum + order.amount),
  );
});

Der Modifier family

Parametrisierte Provider erstellen:

// Product detail provider
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));
});

// Usage
class ProductPage extends ConsumerWidget {
  final String productId;
  
  ProductPage({required this.productId});
  
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final productAsync = ref.watch(productProvider(productId));
    
    return productAsync.when(
      loading: () => CircularProgressIndicator(),
      error: (error, stack) => Text('Error: $error'),
      data: (product) => Text(product.name),
    );
  }
}

Der Modifier autoDispose

Provider automatisch aufräumen:

// Provider is cleaned up when widget is disposed
final searchProvider = FutureProvider.autoDispose<List<Product>>((ref) async {
  // For debounce
  await Future.delayed(Duration(milliseconds: 500));
  
  // Cancellation check
  if (ref.state.isRefreshing) return [];
  
  return await searchProducts();
});

Seiteneffekte mit ref.listen

class MyPage extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // Run side effect when state changes
    ref.listen<int>(counterProvider, (previous, next) {
      if (next == 10) {
        ScaffoldMessenger.of(context).showSnackBar(
          SnackBar(content: Text('You reached 10!')),
        );
      }
    });
    
    return Text('${ref.watch(counterProvider)}');
  }
}

Aktualisieren mit ref.invalidate

// Re-run the provider
ElevatedButton(
  onPressed: () {
    ref.invalidate(userProvider);
  },
  child: Text('Refresh'),
)

Best Practices

1. Definieren Sie Provider global

// ✅ At the top of the file
final counterProvider = StateProvider<int>((ref) => 0);

// ❌ Don't define inside widgets
class MyWidget extends ConsumerWidget {
  final counterProvider = StateProvider<int>((ref) => 0); // Wrong!
}

2. Erstellen Sie kleine, klar umrissene Provider

// ✅ Correct - Each provider does one thing
final userProvider = FutureProvider<User>(...);
final userOrdersProvider = FutureProvider<List<Order>>(...);
final userBalanceProvider = Provider<double>(...);

// ❌ Wrong - Provider doing too much
final everythingProvider = FutureProvider<Everything>(...);

3. Nutzen Sie select, um unnötige Neuaufbauten zu vermeiden

// ✅ Only rebuilds when name changes
final userName = ref.watch(userProvider.select((user) => user.name));

// ❌ Rebuilds when any field of User changes
final user = ref.watch(userProvider);

Zusammenfassung

  • Riverpod: Die Weiterentwicklung von Provider
  • ProviderScope: Der Geltungsbereich, der die Anwendung umschließt
  • StateProvider: Für einfachen Zustand
  • StateNotifierProvider: Für komplexen Zustand
  • FutureProvider: Für asynchrone Daten
  • StreamProvider: Für Daten aus einem Stream
  • ref.watch: In build verwenden (hört mit)
  • ref.read: In Ereignissen verwenden (hört nicht mit)
  • family: Parametrisierter Provider
  • autoDispose: Automatisches Aufräumen

Riverpod ist eine hervorragende Lösung für State Management in großen und komplexen Flutter-Projekten.

Kommentare