Skip to content

dispose_provided_instances

v0.4.0WarningFixConfigurableBloc / Riverpod

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.

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

// Don't — the subscription outlives the provider
import 'package:riverpod/riverpod.dart';
final locationProvider = Provider<StreamSubscription<Position>>((ref) {
final subscription = positionStream().listen(handlePosition); // LINT
return subscription;
});
// Do
import 'package:riverpod/riverpod.dart';
final locationProvider = Provider<StreamSubscription<Position>>((ref) {
final subscription = positionStream().listen(handlePosition);
ref.onDispose(subscription.cancel);
return subscription;
});

build() re-runs on every invalidation, so an unregistered allocation is created again each time and none of the old ones ever stop:

// Don't
import '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++;
}
// Do
import '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++;
}

A tear-off, an arrow lambda and a block body all count:

// All accepted
import '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;
});

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:

many_lints.yaml
rules:
dispose_provided_instances:
additional_cleanup_methods: [shutdown]
// Don't
import 'package:riverpod/riverpod.dart';
class Worker {
void shutdown() {}
}
final workerProvider = Provider<Worker>((ref) {
final worker = Worker(); // LINT
return worker;
});
// Do
import '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.

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.

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:

many_lints.yaml
rules:
dispose_provided_instances: false

To keep the rule on but skip certain paths, use per-rule exclude.

analysis_options.yaml
many_lints:
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”.