
Flutter Riverpod: Why Does My State Reset?
Page navigation cleared your data? Riverpod's autoDispose might be the culprit. Master caching with keepAlive, invalidate, and family modifiers.

Page navigation cleared your data? Riverpod's autoDispose might be the culprit. Master caching with keepAlive, invalidate, and family modifiers.
Yellow stripes appear when the keyboard pops up? Learn how to handle layout overflows using resizeToAvoidBottomInset, SingleChildScrollView, and tricks for chat apps.

Push works on Android but silent on iOS? Learn to fix APNs certificates, handle background messages, configure Notification Channels, and debug FCM integration errors.

Think Android is easier than iOS? Meet Gradle Hell. Learn to fix minSdkVersion conflicts, Multidex limit errors, Namespace issues in Gradle 8.0, and master dependency analysis with `./gradlew dependencies`.

App crashes only in Release mode? It's likely ProGuard/R8. Learn how to debug obfuscated stack traces, use `@Keep` annotations, and analyze `usage.txt`.

I was working on a Product Detail Page for an e-commerce app. The user selected options (Color, Size), clicked 'Back' to browse other items, and returned. All selected options were Reset to Default.
"What? Isn't this Global State? Why did data vanish?"
The culprit was my mindless use of .autoDispose.
Riverpod's superpower is autoDispose.
final productProvider = StateProvider.autoDispose<Product>((ref) => ...);
This modifier makes the Provider smart yet ruthless. "If there are ZERO widgets watching me, I immediately delete myself from memory."
DetailPage. -> Subscriber count: 1 (Created).pop). -> DetailPage destroyed.This is excellent for preventing memory leaks, but poison for "State Persistence".
If data should persist while the app is running (e.g., Cart, User Profile), just remove autoDispose.
final cartProvider = StateProvider<List<Item>>((ref) => []);
Now the state lives until the app terminates. But if you do this for everything, your app's memory usage will explode over time.
ref.keepAlive() (Timeout Caching)"I want to keep it for a while, but not forever." For example, keeping product details while the user browses back and forth effectively, but dropping it if they leave for 5 minutes.
Use ref.keepAlive().
final detailProvider = FutureProvider.autoDispose.family<Detail, int>((ref, id) async {
// 1. Get a keepAlive link
final link = ref.keepAlive();
// 2. Set Timer (e.g., Dispose after 3 mins)
final timer = Timer(const Duration(minutes: 3), () {
link.close(); // 3. Cut the link, allowing Dispose
});
// 4. Cancel timer if re-subscribed
ref.onDispose(() => timer.cancel());
return fetchDetail(id);
});
This implements a Smart Cache strategy: "Cache for at least 3 mins, then auto-release."
invalidate vs refresh)Sometimes you WANT to reset a provider manually. Like "Pull to Refresh".
ref.invalidate(provider): Marks state as "Stale". It re-fetches ONLY when someone tries to read/watch it next time. (Lazy)ref.refresh(provider): Immediately re-executes the logic and returns value. (Eager)Use invalidate usually. Use refresh if you need to await the result right now.
Modern Riverpod uses Code Generation to simplify keepAlive.
By default, all generated providers are autoDispose.
@Riverpod(keepAlive: true)
class UserCarts extends _$UserCarts {
// ... Persists forever (Singleton)
}
@riverpod
Future<Product> productDetail(ProductDetailRef ref, int id) {
// autoDispose by default
ref.keepAlive(); // Keep it alive conditionally!
return fetch(id);
}
Using the Generator eliminates the confusion between Provider, FutureProvider, StateProvider. Just write a function, annotate @riverpod, and let the compiler decide.
Calling API on every keystroke kills your server.
Use ref.onDispose to implement Debouncing.
@riverpod
Future<List<String>> search(SearchRef ref, String query) async {
final cancelToken = CancelToken();
// Cancel previous request if query changes
ref.onDispose(() => cancelToken.cancel());
// Wait 500ms
await Future.delayed(const Duration(milliseconds: 500));
// Check explicitly
if (cancelToken.isCancelled) throw AbortedException();
return fetchResults(query, cancelToken);
}
If user types "apple", requests for a, ap, app, appl get cancelled. Only apple hits the network. Pure magic without RxDart.
Are you still using StateNotifier? Riverpod 2.0 recommends moving to Notifier (Class-based providers).
Why? Because keepAlive, ref, and lifecycle hooks are first-class citizens in Notifier.
Legacy (StateNotifier):
You don't have access to ref inside the class easy unless passed in constructor. Handling onDispose is awkward.
@riverpod
class Counter extends _$Counter {
@override
int build() {
// You can use ref here!
ref.onDispose(() => print("I am dying"));
return 0;
}
void increment() => state++;
}
The code is cleaner, and logic regarding "Lifecycle" (creation/destruction) lives right inside the build() method.
How to verify if your provider resets correctly without manually tapping the UI? Write a Unit Test.
test('State resets on dispose', () {
final container = ProviderContainer();
// Subscribe to keep it alive
final sub = container.listen(counterProvider, (_, __) {});
// 1. Change State
container.read(counterProvider.notifier).increment();
expect(container.read(counterProvider), 1);
// 2. Unsubscribe (Trigger Dispose)
sub.close();
// 3. Re-read (Should be reset)
expect(container.read(counterProvider), 0);
});
Manually controlling ProviderContainer is the best way to master Riverpod's internal mechanism.
Want cleaner code? Ditch ChangeNotifier.
class Auth extends ChangeNotifier {
bool isLoading = false;
void login() {
isLoading = true;
notifyListeners(); // Manual, Error-prone
}
}
Cons: Mutable state, manual notifications, verbose.
After (Notifier):@riverpod
class Auth extends _$Auth {
@override
bool build() => false;
void login() {
state = true; // Auto-notifies listeners
}
}
Pros: Immutable by default, less boilerplate, better performance.
I was building a Multi-step Registration Form. Step 1 (Email) -> Step 2 (Password) -> Step 3 (Profile).
User went from Step 2 back to Step 1. Step 1 was empty.
Why? Step1Page popped -> Provider had 0 listeners -> autoDispose killed it.
Fix:
For Wizards, use Session Persistence (keepAlive or remove autoDispose).
Only clear state when user clicks "Submit" at the very end.
ref.invalidate(formProvider) is your friend.
Riverpod state resetting is not a bug; it's a feature.
autoDispose cleans up. (Garbage Collection)keepAlive() for temporary caching. (Smart Cache)invalidate for pull-to-refresh.
Designing the Lifecycle of Data distinguishes the Senior from the Junior.