Common Mistakes & Production Anti-Patterns
🎯 Senior Engineering Objective
Identify, diagnose, and refactor 8 dangerous production Riverpod anti-patterns. Learn how to conduct code reviews that enforce state safety, unmount guards, value object equality, and strict architectural separation.
1. The Mechanics of State Corruption
2. Mutating State on Unmounted Providers (Missing ref.mounted)
WHY IT BREAKS: If a user navigates away from a screen while an async network request is pending, Riverpod disposes of the provider. Attempting to assign state = result on an unmounted notifier throws a fatal state modification exception.
Future<void> fetchShipments() async {
final result = await repo.getShipments();
- state = AsyncValue.data(result); // BAD: Crashes if user navigated away during await!
+ if (!ref.mounted) return; // GOOD: Protective unmount check
+ state = AsyncValue.data(result);
}
3. Calling ref.read() Inside Widget build() Methods
WHY IT BREAKS: ref.read() is a one-time snapshot inspection that does NOT subscribe to state updates. Calling ref.read(userProvider) inside Widget.build() causes the UI to freeze and fail to rebuild when provider state updates.
@override
Widget build(BuildContext context, WidgetRef ref) {
- final user = ref.read(currentUserProvider); // BAD: Non-reactive read breaks UI!
+ final user = ref.watch(currentUserProvider); // GOOD: Reactive subscription rebuilds UI
return Text(user.value?.fullName ?? '');
}
4. Omitting operator == & hashCode on Family Parameters
WHY IT BREAKS: Family providers use custom parameter objects as Map lookup keys. Without value equality, Dart uses memory addresses (`identical`). Every widget rebuild creates a new instance address, causing Riverpod to trigger infinite REST API fetch loops!
class ShipmentFilters {
final String country;
ShipmentFilters({required this.country});
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || other is ShipmentFilters && country == other.country;
+
+ @override
+ int get hashCode => country.hashCode;
}
5. Performing Navigation or UI Operations in Repositories
WHY IT BREAKS: Repositories must remain pure, stateless data layers. Calling context.go('/home') or showing Snackbars inside a Repository tightly couples data mapping to Flutter UI widgets, breaking unit testability.
// Inside AuthRepository:
- await dio.post('/login'); context.go('/home'); // BAD: UI navigation in repository!
+ await dio.post('/login'); // GOOD: Pure data return
6. Swallowing Exceptions Silently in AsyncNotifier
WHY IT BREAKS: Catching exceptions inside a Notifier and returning dummy fallback values (e.g. empty lists) prevents UI widgets from detecting network errors or rendering error screens via AsyncValue.when().
Future<List<Item>> fetchItems() async {
- try { return await api.getItems(); } catch (e) { return []; } // BAD: Silent swallow!
+ return await AsyncValue.guard(() => api.getItems()); // GOOD: Preserves error state
}
7. Modifying Providers Synchronously During Initial Build
WHY IT BREAKS: Modifying provider B synchronously inside the `build()` method of provider A throws a Flutter/Riverpod assertion exception: "Cannot modify a provider while the provider tree is building".
@override
User build() {
- ref.read(otherProvider.notifier).reset(); // BAD: Synchronous mutation during build!
+ Future.microtask(() { // GOOD: Defer mutation to post-frame
+ ref.read(otherProvider.notifier).reset();
+ });
return initialUser;
}
Special Section J: 15 Senior Code Review & Anti-Pattern Questions
Answer: `ref.read()` performs a non-reactive snapshot lookup. It does NOT register a listener. When the underlying provider state updates, the widget will NOT rebuild, resulting in stale or broken UI states.
Answer: Dart defaults to reference equality (`identical`). On every widget rebuild, a new parameter instance is created with a new memory address. Riverpod treats each address as a new cache key, launching duplicate REST API calls infinitely.
Answer: Riverpod throws "Cannot modify a provider while the provider tree is building". Mutations must be deferred to user gestures or wrapped inside `Future.microtask()`.
📋 Production Code Review Checklist
- ✅ Every async method checks
if (!ref.mounted) return;after anawaitboundary. - ✅ No
ref.read()calls exist insideWidget.build()methods. - ✅ All custom Family parameter classes explicitly override
operator ==andhashCode. - ✅ Repositories contain zero
BuildContext,GoRouter, or UI Snackbar dependencies. - ✅ Monetary values are processed strictly as
intEUR cents.