dispose_provided_instances
This rule flags a local variable created inside a Riverpod provider callback or a Notifier build() whose type declares dispose(), close() or cancel(), when no matching ref.onDispose(...) registers its cleanup. A quick fix inserts the ref.onDispose call.
Why use this rule
Section titled “Why use this rule”A provider is torn down when nothing watches it any more — an autoDispose provider on every navigation, a family entry when its argument goes out of use. Anything the provider allocated goes with it only if it was registered: Riverpod has no way to know that the StreamSubscription you opened belongs to this provider unless ref.onDispose says so.
Without it the subscription keeps firing, the timer keeps ticking and the controller keeps its listeners — against a provider that no longer exists. The symptom is usually a callback running after the screen is gone, not an obvious leak.
See also: Riverpod automatic disposal
Examples
Section titled “Examples”A subscription opened in a provider
Section titled “A subscription opened in a provider”// Don't — the subscription outlives the providerimport 'package:riverpod/riverpod.dart';
final locationProvider = Provider<StreamSubscription<Position>>((ref) { final subscription = positionStream().listen(handlePosition); // LINT return subscription;});// Doimport 'package:riverpod/riverpod.dart';
final locationProvider = Provider<StreamSubscription<Position>>((ref) { final subscription = positionStream().listen(handlePosition); ref.onDispose(subscription.cancel); return subscription;});The same thing in a Notifier’s build()
Section titled “The same thing in a Notifier’s build()”build() re-runs on every invalidation, so an unregistered allocation is created again each time and none of the old ones ever stop:
// Don'timport 'package:riverpod/riverpod.dart';
class ClockNotifier extends Notifier<int> { @override int build() { final timer = Timer.periodic(const Duration(seconds: 1), _tick); // LINT return 0; }
void _tick(Timer timer) => state++;}// Doimport 'package:riverpod/riverpod.dart';
class ClockNotifier extends Notifier<int> { @override int build() { final timer = Timer.periodic(const Duration(seconds: 1), _tick); ref.onDispose(timer.cancel); return 0; }
void _tick(Timer timer) => state++;}Three accepted forms of registration
Section titled “Three accepted forms of registration”A tear-off, an arrow lambda and a block body all count:
// All acceptedimport 'package:riverpod/riverpod.dart';
final aProvider = Provider<HttpClient>((ref) { final client = HttpClient(); ref.onDispose(client.close); // tear-off return client;});
final bProvider = Provider<HttpClient>((ref) { final client = HttpClient(); ref.onDispose(() => client.close()); // arrow lambda return client;});
final cProvider = Provider<HttpClient>((ref) { final client = HttpClient(); ref.onDispose(() { // block body client.close(); }); return client;});A project’s own cleanup method
Section titled “A project’s own cleanup method”If your types shut down through a name Riverpod does not know, add it. additional_cleanup_methods extends the standard list rather than replacing it:
rules: dispose_provided_instances: additional_cleanup_methods: [shutdown]// Don'timport 'package:riverpod/riverpod.dart';
class Worker { void shutdown() {}}
final workerProvider = Provider<Worker>((ref) { final worker = Worker(); // LINT return worker;});// Doimport 'package:riverpod/riverpod.dart';
class Worker { void shutdown() {}}
final workerProvider = Provider<Worker>((ref) { final worker = Worker(); ref.onDispose(worker.shutdown); return worker;});Names added this way are appended, so a type declaring both close() and your shutdown() is still expected to be closed — the standard names win.
Known limitations
Section titled “Known limitations”Only a local variable declared inside the callback is tracked. An instance created and returned in one expression (Provider((ref) => HttpClient())) has no name to register, so it is not reported.
Registration is matched by the variable’s name and cleanup method, textually: ref.onDispose(client.close) disposes client, but a helper that takes the instance and registers it internally (registerCleanup(ref, client)) is not recognised, and the variable is reported as undisposed.
Turning this rule off
Section titled “Turning this rule off”This rule is in the recommended preset, so it is on with
preset: recommended or preset: opinionated. Add it to preset: core with
dispose_provided_instances: true.
To turn it off:
rules: dispose_provided_instances: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Options
Section titled “Options”many_lints: rules: dispose_provided_instances: additional_cleanup_methods: [release, shutdown]rules: dispose_provided_instances: additional_cleanup_methods: [release, shutdown]| Option | Type | Default | Description |
|---|---|---|---|
cleanup_methods |
list of strings | [dispose, close, cancel] |
Replaces the cleanup method names the rule looks for |
additional_cleanup_methods |
list of strings | [] |
Extends whichever list applies |
Order matters: it is the priority used when a type declares more than one
cleanup method. Names added via additional_cleanup_methods are appended, so a
project’s own release() is only chosen when the type declares no standard
cleanup method.
Both options apply to detection and recognition — a method listed here counts
both as “this instance needs disposing” and as “this ref.onDispose disposes it”.
Related rules
Section titled “Related rules”dispose_fields— Ensure State fields with disposal methods are cleaned up in dispose().always_remove_listener— Ensure every addListener() has a matching removeListener() in dispose().avoid_unremovable_callbacks_in_listeners— Don’t pass an inline closure to addListener.emit_new_bloc_state_instances— Emit a new state instance instead of the existing state object.