Riverpod ref Usage in Provider Lifecycle Callbacks
SkillCommunicationFix Riverpod "Cannot use Ref or modify other providers inside life-cycles/selectors" crash in @riverpod provider bodies. Use when: (1) Crash during provider disposal with this exact error message, (2) Using keepAlive() with timer-based disposal, (3) Async callbacks (.then, .catchError) try to use ref.read() or ref.invalidateSelf(), (4) Error occurs after ref.onCancel/onResume/onDispose callbacks fire. Solution: wrap ref operations in try-catch or avoid using ref in async callbacks entirely.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Riverpod ref Usage in Provider Lifecycle Callbacks skill
What this skill tells your AI
The instructions your AI receives, as published by divinevideo/divine-mobile in .agents/skills/riverpod-ref-in-provider-lifecycle/SKILL.md and read by ahel’s review.
Problem
When using ref.keepAlive() with timer-based auto-disposal in @riverpod providers,
async callbacks (.then(), .catchError()) that execute during or after disposal
will crash when they try to use ref.read(), ref.invalidateSelf(), or any other
ref operation.
Context / Trigger Conditions
Error message:
'package:riverpod/src/core/ref.dart': Failed assertion: line 216 pos 7:
'_debugCallbackStack == 0': Cannot use Ref or modify other providers inside life-cycles/selectors.
Typical scenario:
- Provider uses
ref.keepAlive()with a timer inref.onCancel():
final link = ref.keepAlive();
ref.onCancel(() {
Timer(Duration(seconds: 15), () {
link.close(); // Triggers disposal
});
});
- Provider has async callbacks that use
ref:
someAsyncOperation().then((_) {
if (ref.mounted) { // THIS CHECK IS NOT ENOUGH!
ref.invalidateSelf();
}
});
- Timer fires while async callback is pending
- CRASH -
ref.mountedreturnstruebut usingrefis still forbidden
Key insight: ref.mounted returns true during lifecycle callbacks, but
using ref operations is still forbidden. This is counter-intuitive but by design.
Solution
Option 1: Wrap ref operations in try-catch (Recommended)
someAsyncOperation().then((_) {
try {
ref.read(someProvider.notifier).update(/*...*/);
} catch (e) {
Log.debug('Provider likely disposed: $e');
}
});
Option 2: Avoid ref operations in async callbacks entirely
Instead of:
// BAD - uses ref in async callback
openVineVideoCache.removeCorruptedVideo(videoId).then((_) {
if (ref.mounted) {
ref.invalidateSelf(); // CRASH!
}
});
Do:
// GOOD - fire-and-forget without ref
unawaited(
openVineVideoCache.removeCorruptedVideo(videoId).then((_) {
Log.info('Cache removed'); // No ref usage
}),
);
// Let user retry manually or provider recreates on next access
Option 3: Capture provider state synchronously first
// Read state BEFORE async operation
final notifier = ref.read(someProvider.notifier);
// Use captured reference in callback (not ref)
someAsyncOperation().then((_) {
notifier.doSomething(); // Uses captured reference, not ref
});
Anti-Pattern: ref.mounted Check
// THIS DOES NOT WORK!
someAsyncOperation().then((_) {
if (ref.mounted) { // Returns true during lifecycle!
ref.invalidateSelf(); // Still crashes!
}
});
The ref.mounted check is insufficient because:
- Timer fires,
link.close()called - Riverpod enters disposal lifecycle (callback stack > 0)
ref.mountedcheck passes (still returnstrue!)ref.invalidateSelf()called- CRASH - ref operations forbidden during lifecycle callbacks
Verification
- Rapidly scroll through content that uses the provider
- Navigate away and back while providers are active
- Let the keepAlive timers fire naturally (wait 15+ seconds after scrolling)
- Check Crashlytics/console for the lifecycle assertion error
- Error should no longer occur
Example
Before (causes crash):
@riverpod
VideoPlayerController videoController(Ref ref, String videoId) {
final link = ref.keepAlive();
ref.onCancel(() {
Timer(Duration(seconds: 15), () => link.close());
});
final controller = VideoPlayerController.networkUrl(url);
controller.initialize().catchError((error) {
// CRASH! This callback may run during disposal
if (ref.mounted) {
ref.invalidateSelf(); // Assertion failure!
}
});
return controller;
}
After (safe):
@riverpod
VideoPlayerController videoController(Ref ref, String videoId) {
final link = ref.keepAlive();
ref.onCancel(() {
Timer(Duration(seconds: 15), () => link.close());
});
final controller = VideoPlayerController.networkUrl(url);
controller.initialize().catchError((error) {
// Safe - wrapped in try-catch
try {
ref.read(fallbackProvider.notifier).state = newValue;
} catch (e) {
Log.debug('Provider disposed during error handling: $e');
}
// Don't invalidateSelf - let provider recreate on next access
});
return controller;
}
Notes
- This issue is specific to
@riverpodprovider bodies, not widget dispose() - The related skill
riverpod-ref-read-in-disposecovers widget lifecycle issues - Consider whether
ref.invalidateSelf()is even necessary - often the provider will be recreated naturally on next access - For truly critical cleanup, use
ref.onDispose()which runs synchronously before the lifecycle callback stack check - This bug is particularly common with video players, image loaders, and other resources that use keepAlive() with timer-based disposal
Related Skills
riverpod-ref-read-in-dispose: For widget dispose() ref.read() issuesflutter-dispose-timer-test-failure: For timer-related test failures
References
Signals
- GitHub stars
- 265
- Forks
- 55
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
riverpod-ref-in-provider-lifecycle- Source
- github.com/divinevideo/divine-mobile