Phase 12

Common Mistakes & Production Anti-Patterns

Calculating reading time... Module 3: Advanced & Audit

🎯 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

Dangerous Anti-Pattern Traps
Missing ref.mounted
State update after unmount
Missing operator ==
Infinite REST fetch loop
ref.read in build()
Non-reactive frozen UI
↓ Breaks Runtime Contracts
Production Failure Consequences
Fatal Exception
Bad state mutation crash
API Spam
Exhausts network quota
Stale UI
Widget misses updates

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.

Diff Anti-Pattern #1 Remedy
  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.

Diff Anti-Pattern #2 Remedy
  @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!

Diff Anti-Pattern #3 Remedy
  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.

Diff Anti-Pattern #4 Remedy
// 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().

Diff Anti-Pattern #5 Remedy
  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".

Diff Anti-Pattern #6 Remedy
  @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 an await boundary.
  • ✅ No ref.read() calls exist inside Widget.build() methods.
  • ✅ All custom Family parameter classes explicitly override operator == and hashCode.
  • ✅ Repositories contain zero BuildContext, GoRouter, or UI Snackbar dependencies.
  • ✅ Monetary values are processed strictly as int EUR cents.