Riverpod ref Usage in Provider Lifecycle Callbacks

SkillCommunication

Fix 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.

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:

  1. Provider uses ref.keepAlive() with a timer in ref.onCancel():
final link = ref.keepAlive();
ref.onCancel(() {
  Timer(Duration(seconds: 15), () {
    link.close();  // Triggers disposal
  });
});
  1. Provider has async callbacks that use ref:
someAsyncOperation().then((_) {
  if (ref.mounted) {  // THIS CHECK IS NOT ENOUGH!
    ref.invalidateSelf();
  }
});
  1. Timer fires while async callback is pending
  2. CRASH - ref.mounted returns true but using ref is 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:

  1. Timer fires, link.close() called
  2. Riverpod enters disposal lifecycle (callback stack > 0)
  3. ref.mounted check passes (still returns true!)
  4. ref.invalidateSelf() called
  5. CRASH - ref operations forbidden during lifecycle callbacks

Verification

  1. Rapidly scroll through content that uses the provider
  2. Navigate away and back while providers are active
  3. Let the keepAlive timers fire naturally (wait 15+ seconds after scrolling)
  4. Check Crashlytics/console for the lifecycle assertion error
  5. 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 @riverpod provider bodies, not widget dispose()
  • The related skill riverpod-ref-read-in-dispose covers 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() issues
  • flutter-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