Fortgeschrittene Riverpod-Patterns: Families, Notifiers und Testing

12 Min. Lesezeit9. März 2026
Riverpod advancedRiverpod familiesAsyncNotifier FlutterRiverpod testingriverpod_generatorFlutter state management advancedRiverpod best practicesRiverpod code generation

# Fortgeschrittene Riverpod-Patterns fuer produktionsreife Flutter-Apps

Riverpod ist meine Standard-Loesung fuer State Management in Flutter geworden. Doch zwischen dem Einstieg in Riverpod und dem professionellen Einsatz in Produktionsumgebungen liegt ein erheblicher Unterschied. In diesem Artikel teile ich die Patterns, Fallstricke und Architekturentscheidungen, die ich ueber mehrere veroeffentlichte Apps hinweg verfeinert habe.

Provider Families und parametrisierte Provider

Mit Provider Families lassen sich Provider erstellen, die Parameter entgegennehmen. Das ist unverzichtbar, wenn dieselbe Datenabruf-Logik fuer unterschiedliche Eingaben gilt -- etwa beim Laden eines Nutzerprofils anhand einer ID oder beim Abrufen von Produkten nach Kategorie.

Grundlegende Family-Nutzung

dart
final userProvider = FutureProvider.autoDispose.family<User, String>(
  (ref, userId) async {
    final api = ref.watch(apiClientProvider);
    return api.getUser(userId);
  },
);

class=class="code-string">"code-comment">// Im Widget
class UserProfileScreen extends ConsumerWidget {
  final String userId;
  const UserProfileScreen({super.key, required this.userId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final userAsync = ref.watch(userProvider(userId));
    return userAsync.when(
      loading: () => const LoadingIndicator(),
      error: (err, stack) => ErrorDisplay(message: err.toString()),
      data: (user) => UserProfileView(user: user),
    );
  }
}

Komplexe Parameter mit Records

Wenn ein Provider mehrere Parameter benoetigt, bieten Dart Records eine elegante Loesung -- ohne eine eigene Parameterklasse anlegen zu muessen.

dart
typedef ProductFilter = ({String category, int page, SortOrder sort});

final productsProvider = FutureProvider.autoDispose
    .family<List<Product>, ProductFilter>((ref, filter) async {
  final api = ref.watch(apiClientProvider);
  return api.getProducts(
    category: filter.category,
    page: filter.page,
    sort: filter.sort,
  );
});

class=class="code-string">"code-comment">// Verwendung
final products = ref.watch(productsProvider((
  category: class="code-string">'electronics',
  page: class="code-number">1,
  sort: SortOrder.priceAsc,
)));

In meinen Produktions-Apps habe ich festgestellt, dass Records die perfekte Balance bieten: Sie liefern benannte Felder ohne den Overhead einer vollstaendigen Klasse, und Riverpod behandelt den Gleichheitsvergleich direkt korrekt.

StateNotifier und AsyncNotifier Patterns

Wann StateNotifier noch sinnvoll ist

Obwohl `Notifier` und `AsyncNotifier` seit Riverpod 2.0 der empfohlene Ansatz sind, werden Sie in bestehenden Codebasen auf `StateNotifier` stossen. Beide Varianten zu verstehen ist wichtig.

dart
class=class="code-string">"code-comment">// Legacy StateNotifier-Ansatz
class CartNotifier extends StateNotifier<List<CartItem>> {
  final ApiClient _api;
  CartNotifier(this._api) : super([]);

  Future<void> addItem(Product product) async {
    state = [...state, CartItem.fromProduct(product)];
    await _api.syncCart(state);
  }

  void removeItem(String itemId) {
    state = state.where((item) => item.id != itemId).toList();
  }
}

final cartProvider =
    StateNotifierProvider<CartNotifier, List<CartItem>>((ref) {
  return CartNotifier(ref.watch(apiClientProvider));
});

Das moderne AsyncNotifier-Pattern

Bei asynchronen Workflows zeigt Riverpod mit `AsyncNotifier` seine wahre Staerke. Die `build`-Methode definiert das initiale asynchrone Laden, und nachfolgende Mutationen koennen den State aktualisieren, waehrend die Fehlerbehandlung erhalten bleibt.

dart
class ProductListNotifier
    extends AsyncNotifier<List<Product>> {
  @override
  Future<List<Product>> build() async {
    final api = ref.watch(apiClientProvider);
    return api.fetchProducts();
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build());
  }

  Future<void> deleteProduct(String id) async {
    final previousState = state;
    class=class="code-string">"code-comment">// Optimistisches Update
    state = AsyncData(
      state.value!.where((p) => p.id != id).toList(),
    );
    try {
      await ref.read(apiClientProvider).deleteProduct(id);
    } catch (e, stack) {
      class=class="code-string">"code-comment">// Rollback bei Fehler
      state = previousState;
      state = AsyncError(e, stack);
    }
  }
}

final productListProvider =
    AsyncNotifierProvider<ProductListNotifier, List<Product>>(
  ProductListNotifier.new,
);

In meinen Produktions-Apps hat sich das optimistische Update-Pattern als unverzichtbar erwiesen, um reaktionsfaehige UIs zu bauen. Der Nutzer bekommt sofortiges Feedback, waehrend die Netzwerkanfrage im Hintergrund laeuft -- und bei Fehlern wird der State sauber zurueckgesetzt.

Family + AsyncNotifier Kombination

dart
class UserOrdersNotifier
    extends FamilyAsyncNotifier<List<Order>, String> {
  @override
  Future<List<Order>> build(String userId) async {
    final api = ref.watch(apiClientProvider);
    return api.getUserOrders(userId);
  }

  Future<void> cancelOrder(String orderId) async {
    await ref.read(apiClientProvider).cancelOrder(orderId);
    ref.invalidateSelf();
    await future;
  }
}

final userOrdersProvider = AsyncNotifierProvider.family<
    UserOrdersNotifier, List<Order>, String>(
  UserOrdersNotifier.new,
);

Welchen Provider-Typ wann verwenden?

Die Wahl des richtigen Provider-Typs ist eine der ersten Entscheidungen, an der Entwickler straucheln. Hier ist die Entscheidungstabelle, die ich verwende.

| Szenario | Provider-Typ | Grund |

|---|---|---|

| Einfacher abgeleiteter Wert | `Provider` | Berechneter State, keine Seiteneffekte |

| Asynchroner Datenabruf (nur lesen) | `FutureProvider` | Automatische Lade-/Fehler-/Datenzustaende |

| Echtzeit-Stream | `StreamProvider` | WebSocket, Firestore Snapshots |

| Veraenderbarer synchroner State | `NotifierProvider` | Formularzustand, Toggles, Zaehler |

| Veraenderbarer asynchroner State | `AsyncNotifierProvider` | CRUD-Operationen mit Ladezustaenden |

| Parametrisiert (nur lesen) | `FutureProvider.family` | Gleiche Abfrage, unterschiedliche Eingaben |

| Parametrisiert (veraenderbar) | `AsyncNotifierProvider.family` | Entitaetsbezogene Operationen |

| Einmalige Einrichtung | `Provider` | Dependency Injection fuer Services |

Im Zweifelsfall: Beginnen Sie mit `FutureProvider` fuer Leseoperationen und `AsyncNotifierProvider` fuer Schreiboperationen. Refactoring ist jederzeit moeglich.

Codegenerierung mit riverpod_generator

Codegenerierung reduziert Boilerplate und faengt Konfigurationsfehler zur Kompilierzeit ab. In meinen Produktions-Apps habe ich festgestellt, dass die fruehe Einfuehrung von Codegen spaeter erheblichen Refactoring-Aufwand spart.

Einrichtung

yaml
# pubspec.yaml
dependencies:
  flutter_riverpod: ^2.5.0
  riverpod_annotation: ^2.3.0

dev_dependencies:
  riverpod_generator: ^2.4.0
  build_runner: ^2.4.0

Generierte Provider in der Praxis

dart
import class="code-string">'package:riverpod_annotation/riverpod_annotation.dart';

part class="code-string">'auth_provider.g.dart';

class=class="code-string">"code-comment">// Einfacher Provider — ersetzt Provider<ApiClient>
@riverpod
ApiClient apiClient(ApiClientRef ref) {
  return ApiClient(baseUrl: class="code-string">'https:class=class="code-string">"code-comment">//api.example.com');
}

class=class="code-string">"code-comment">// FutureProvider mit Family — ersetzt FutureProvider.family
@riverpod
Future<User> userProfile(UserProfileRef ref, String userId) async {
  final api = ref.watch(apiClientProvider);
  return api.getUser(userId);
}

class=class="code-string">"code-comment">// AsyncNotifier — ersetzt AsyncNotifierProvider
@riverpod
class TodoList extends _$TodoList {
  @override
  Future<List<Todo>> build() async {
    final api = ref.watch(apiClientProvider);
    return api.fetchTodos();
  }

  Future<void> addTodo(String title) async {
    final api = ref.read(apiClientProvider);
    final newTodo = await api.createTodo(title);
    state = AsyncData([...state.value ?? [], newTodo]);
  }
}

KeepAlive und AutoDispose-Steuerung

dart
class=class="code-string">"code-comment">// Dieser Provider bleibt waehrend der gesamten App-Laufzeit aktiv
@Riverpod(keepAlive: true)
class AuthState extends _$AuthState {
  @override
  Future<AuthUser?> build() async {
    return ref.watch(authRepositoryProvider).getCurrentUser();
  }
}

class=class="code-string">"code-comment">// Dieser Provider wird automatisch bereinigt, wenn er nicht mehr beobachtet wird
@riverpod
Future<List<Product>> searchProducts(
  SearchProductsRef ref,
  String query,
) async {
  class=class="code-string">"code-comment">// Suche entprellen
  await Future.delayed(const Duration(milliseconds: class="code-number">300));
  if (ref.state is AsyncLoading) return [];

  final api = ref.watch(apiClientProvider);
  return api.searchProducts(query);
}

Provider testen

Beim Testen zahlt sich die Architektur von Riverpod richtig aus. Sie brauchen keine Widget-Trees, keinen BuildContext und keine komplexen Mocking-Frameworks. Ein `ProviderContainer` genuegt.

Unit-Tests fuer Notifier

dart
void main() {
  late ProviderContainer container;
  late MockApiClient mockApi;

  setUp(() {
    mockApi = MockApiClient();
    container = ProviderContainer(
      overrides: [
        apiClientProvider.overrideWithValue(mockApi),
      ],
    );
    addTearDown(container.dispose);
  });

  test(class="code-string">'fetchProducts gibt Produktliste zurueck', () async {
    final expectedProducts = [
      Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Widget A'),
      Product(id: class="code-string">'class="code-number">2', name: class="code-string">'Widget B'),
    ];
    when(() => mockApi.fetchProducts())
        .thenAnswer((_) async => expectedProducts);

    final subscription = container.listen(
      productListProvider,
      (_, __) {},
    );

    await container.read(productListProvider.future);

    expect(
      container.read(productListProvider).value,
      equals(expectedProducts),
    );

    subscription.close();
  });

  test(class="code-string">'deleteProduct fuehrt optimistisches Update durch', () async {
    when(() => mockApi.fetchProducts())
        .thenAnswer((_) async => [Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Test')]);
    when(() => mockApi.deleteProduct(class="code-string">'class="code-number">1'))
        .thenAnswer((_) async {});

    container.listen(productListProvider, (_, __) {});
    await container.read(productListProvider.future);

    await container
        .read(productListProvider.notifier)
        .deleteProduct(class="code-string">'class="code-number">1');

    expect(
      container.read(productListProvider).value,
      isEmpty,
    );
  });
}

Tests mit mehreren Overrides

dart
ProviderContainer createTestContainer({
  AuthUser? currentUser,
  List<Product>? products,
}) {
  return ProviderContainer(
    overrides: [
      authStateProvider.overrideWith(() => MockAuthNotifier(currentUser)),
      productListProvider.overrideWith(() => MockProductNotifier(products)),
      analyticsProvider.overrideWithValue(MockAnalytics()),
    ],
  );
}

test(class="code-string">'Admin-Nutzer kann Produkte loeschen', () async {
  final container = createTestContainer(
    currentUser: AuthUser(role: UserRole.admin),
    products: [Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Test')],
  );
  addTearDown(container.dispose);

  final canDelete = container.read(canDeleteProductProvider);
  expect(canDelete, isTrue);
});

Widget-Tests mit ProviderScope-Overrides

dart
testWidgets(class="code-string">'ProductListScreen zeigt Laden und dann Daten', (tester) async {
  await tester.pumpWidget(
    ProviderScope(
      overrides: [
        productListProvider.overrideWith(
          () => FakeProductNotifier([
            Product(id: class="code-string">'class="code-number">1', name: class="code-string">'Testprodukt'),
          ]),
        ),
      ],
      child: const MaterialApp(home: ProductListScreen()),
    ),
  );

  class=class="code-string">"code-comment">// Anfangs wird der Ladeindikator angezeigt
  expect(find.byType(CircularProgressIndicator), findsOneWidget);

  class=class="code-string">"code-comment">// Nach Abschluss der asynchronen Operation
  await tester.pumpAndSettle();
  expect(find.text(class="code-string">'Testprodukt'), findsOneWidget);
});

Riverpod + Clean Architecture Integration

Clean Architecture und Riverpod ergaenzen sich hervorragend, wenn klare Grenzen definiert werden. Hier ist die Struktur, die ich in Produktionsprojekten verwende.

Schichtentrennung

lib/
  features/
    products/
      data/
        datasources/
          product_remote_datasource.dart
          product_local_datasource.dart
        models/
          product_model.dart
        repositories/
          product_repository_impl.dart
      domain/
        entities/
          product.dart
        repositories/
          product_repository.dart       # abstrakt
        usecases/
          get_products.dart
          delete_product.dart
      presentation/
        providers/
          product_providers.dart
        screens/
          product_list_screen.dart
        widgets/
          product_card.dart

Verdrahtung mit Providern

dart
class=class="code-string">"code-comment">// Datenschicht — konkrete Implementierungen
@riverpod
ProductRemoteDataSource productRemoteDataSource(
  ProductRemoteDataSourceRef ref,
) {
  final dio = ref.watch(dioProvider);
  return ProductRemoteDataSourceImpl(dio);
}

class=class="code-string">"code-comment">// Domaenschicht — abstraktes Repository ueber Datenschicht bereitgestellt
@riverpod
ProductRepository productRepository(ProductRepositoryRef ref) {
  return ProductRepositoryImpl(
    remote: ref.watch(productRemoteDataSourceProvider),
    local: ref.watch(productLocalDataSourceProvider),
    network: ref.watch(networkInfoProvider),
  );
}

class=class="code-string">"code-comment">// Use Cases
@riverpod
GetProducts getProducts(GetProductsRef ref) {
  return GetProducts(ref.watch(productRepositoryProvider));
}

class=class="code-string">"code-comment">// Praesentationsschicht — verbindet Use Cases mit dem UI-State
@riverpod
class ProductListController extends _$ProductListController {
  @override
  Future<List<Product>> build() async {
    final getProducts = ref.watch(getProductsProvider);
    final result = await getProducts(NoParams());
    return result.fold(
      (failure) => throw failure,
      (products) => products,
    );
  }
}

Die zentrale Erkenntnis: Provider fungieren als Ihr Dependency-Injection-Container. Jede Schicht haengt nur von den Abstraktionen der darunterliegenden Schicht ab, und Riverpod verbindet die konkreten Implementierungen miteinander.

Haeufige Riverpod Anti-Patterns

1. ref.read in build-Methoden verwenden

dart
class=class="code-string">"code-comment">// FALSCH: ref.read loest bei Wertaenderungen keinen Rebuild aus
@override
Widget build(BuildContext context, WidgetRef ref) {
  final user = ref.read(userProvider); class=class="code-string">"code-comment">// Bug: wird nicht aktualisiert
  return Text(user.name);
}

class=class="code-string">"code-comment">// RICHTIG: ref.watch baut das Widget bei Wertaenderungen neu auf
@override
Widget build(BuildContext context, WidgetRef ref) {
  final user = ref.watch(userProvider);
  return Text(user.name);
}

2. ref.watch innerhalb von Callbacks

dart
class=class="code-string">"code-comment">// FALSCH: ref.watch im Callback verursacht unvorhersehbares Verhalten
onPressed: () {
  final api = ref.watch(apiProvider); class=class="code-string">"code-comment">// Nicht in Callbacks watchen
  api.submit();
}

class=class="code-string">"code-comment">// RICHTIG: ref.read fuer einmalige Aktionen in Callbacks
onPressed: () {
  final api = ref.read(apiProvider);
  api.submit();
}

3. Aufgeblaehte Provider, die alles machen

dart
class=class="code-string">"code-comment">// FALSCH: ein Provider fuer Auth, Profil, Einstellungen und Benachrichtigungen
@riverpod
class AppState extends _$AppState {
  @override
  Future<AppData> build() async {
    final user = await fetchUser();
    final settings = await fetchSettings();
    final notifications = await fetchNotifications();
    return AppData(user, settings, notifications);
  }
}

class=class="code-string">"code-comment">// RICHTIG: Verantwortlichkeiten in fokussierte Provider aufteilen
@riverpod
Future<User> currentUser(CurrentUserRef ref) async { ... }

@riverpod
Future<Settings> userSettings(UserSettingsRef ref) async { ... }

@riverpod
Future<List<Notification>> notifications(NotificationsRef ref) async { ... }

4. autoDispose bei navigationsbezogenen Daten vergessen

dart
class=class="code-string">"code-comment">// FALSCH: dieser Provider verursacht ein Speicherleck nach Navigation
final searchResultsProvider = FutureProvider<List<Product>>((ref) async {
  return ref.watch(apiProvider).search(ref.watch(searchQueryProvider));
});

class=class="code-string">"code-comment">// RICHTIG: autoDispose raeumt auf, wenn der Screen nicht mehr sichtbar ist
final searchResultsProvider =
    FutureProvider.autoDispose<List<Product>>((ref) async {
  return ref.watch(apiProvider).search(ref.watch(searchQueryProvider));
});

5. Zirkulaere Provider-Abhaengigkeiten

dart
class=class="code-string">"code-comment">// FALSCH: providerA beobachtet providerB, der providerA beobachtet
class=class="code-string">"code-comment">// Das verursacht zur Laufzeit eine ProviderException

class=class="code-string">"code-comment">// RICHTIG: Abhaengigkeiten so umstrukturieren, dass sie in eine Richtung fliessen
class=class="code-string">"code-comment">// Gemeinsame Logik in einen dritten Provider auslagern

In meinen Produktions-Apps habe ich festgestellt, dass Anti-Pattern Nummer 3 in der Praxis den meisten Schmerz verursacht. Wenn ein einziger riesiger Provider mehrere Verantwortlichkeiten traegt, erzwingt eine Aenderung bei Benachrichtigungen ein unnoeties Neuladen des Nutzerprofils. Kleine, fokussierte Provider lassen Riverpods Abhaengigkeitsgraphen effizient arbeiten.

Tipps zur Leistungsoptimierung

Gezielte Rebuilds mit select

dart
class=class="code-string">"code-comment">// Statt bei jeder Nutzeraenderung neu zu bauen
final user = ref.watch(userProvider);

class=class="code-string">"code-comment">// Nur bei Aenderung eines bestimmten Feldes neu bauen
final userName = ref.watch(
  userProvider.select((user) => user.name),
);

Debouncing von Provider-Rebuilds

dart
@riverpod
Future<List<Product>> searchProducts(
  SearchProductsRef ref,
  String query,
) async {
  class=class="code-string">"code-comment">// Abbrechen, wenn innerhalb von 300ms eine neue Anfrage kommt
  ref.keepAlive();
  final link = ref.keepAlive();
  final timer = Timer(const Duration(seconds: class="code-number">30), link.close);
  ref.onDispose(timer.cancel);

  class=class="code-string">"code-comment">// Entprellen
  await Future.delayed(const Duration(milliseconds: class="code-number">300));

  final api = ref.watch(apiClientProvider);
  return api.search(query);
}

Fazit

Riverpod belohnt durchdachte Architektur. Die Patterns in diesem Artikel -- parametrisierte Families, AsyncNotifier mit optimistischen Updates, Codegenerierung, fokussierte Provider mit Clean-Architecture-Grenzen -- bilden ein Werkzeugset, das von einem kleinen Feature bis zur komplexen Produktions-App skaliert. Allein die Testbarkeit, bei der ein `ProviderContainer` mit Overrides hunderte Zeilen Mock-Setup ersetzt, macht Riverpod zur lohnenden Investition.

Beginnen Sie mit der Entscheidungstabelle fuer den richtigen Provider-Typ, fuehren Sie Codegenerierung frueh ein, halten Sie Ihre Provider klein und fokussiert -- und Ihre Flutter-Apps werden eine Freude in der Wartung sein.

Kontaktieren Sie mich, wenn Sie Unterstuetzung beim Entwurf einer Riverpod-Architektur oder bei der Migration einer bestehenden Codebasis auf moderne Riverpod-Patterns benoetigen.

Verwandte Artikel

Haben Sie ein Flutter-Projekt?

Ich entwickle hochleistungsfähige Flutter-Anwendungen für iOS, Android und Web.

Kontakt aufnehmen