avoid_ref_inside_state_dispose
v0.4.0 Warning Riverpod State
This rule catches ref usage inside the dispose() method of ConsumerState classes. By the time dispose() runs, providers may already be torn down, so reading or watching them can throw unexpected errors or return stale data.
Why use this rule
Section titled “Why use this rule”In Riverpod, the lifecycle of providers and widgets is not tightly coupled. When dispose() fires, there is no guarantee that the providers you are trying to access are still alive. Accessing ref in dispose() can silently read disposed state or throw UnmountedRefException, leading to hard-to-debug crashes in production.
See also: Riverpod automatic disposal
The usual shape: a dispose() that tries to tell a provider the screen is
gone. By the time it runs the provider may already have been torn down, so this
either throws or writes to state nothing is listening to.
class _EditorPageState extends ConsumerState<EditorPage> { final _controller = TextEditingController();
@override void dispose() { ref.read(draftProvider.notifier).save(_controller.text); // LINT _controller.dispose(); super.dispose(); }
@override Widget build(BuildContext context) => TextField(controller: _controller);}Dispose only what this widget owns, and push the provider work to the moment
the user actually leaves — or let the provider clean up after itself with
ref.onDispose:
class _EditorPageState extends ConsumerState<EditorPage> { final _controller = TextEditingController();
void _saveDraft() { ref.read(draftProvider.notifier).save(_controller.text); }
@override void dispose() { _controller.dispose(); super.dispose(); }
@override Widget build(BuildContext context) => TextField( controller: _controller, onSubmitted: (_) => _saveDraft(), );}Reading a value you need at dispose time
Section titled “Reading a value you need at dispose time”Capture it while the widget is alive, then use the captured value in
dispose():
class _SessionPageState extends ConsumerState<SessionPage> { late final Analytics _analytics = ref.read(analyticsProvider);
@override void dispose() { _analytics.screenClosed('session'); // no ref here super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}Known limitations
Section titled “Known limitations”The search stops at a closure boundary. A ref read inside a callback declared
in dispose() is not reported, because such a callback may be stored and run
somewhere else entirely.
Only dispose() on a ConsumerState subclass is checked. deactivate() has
the same hazard and is not covered.
Configuration
Section titled “Configuration”This rule is in the recommended preset, so it is on with
preset: recommended or preset: opinionated. Add it to preset: core with
avoid_ref_inside_state_dispose: true.
To turn it off:
rules: avoid_ref_inside_state_dispose: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Related rules
Section titled “Related rules”avoid_ref_read_inside_build— Subscribe in build; do not read once.avoid_ref_watch_outside_build— Subscribe only in build; read once everywhere else.use_ref_and_state_synchronously— Check ref.mounted before using ref or state after an await.async_value_nullable_pattern— Matching AsyncValue(:final value?) on a nullable value hides a legitimate null result.