dispose_fields
Flags instance fields in State subclasses whose type has a dispose(), close(), or cancel() method but the field is not cleaned up in the widget’s dispose() method. Common types include TextEditingController, FocusNode, AnimationController, StreamController, StreamSubscription, and Timer.
Why use this rule
Section titled “Why use this rule”Disposable resources that are not cleaned up cause memory leaks. A TextEditingController that is never disposed keeps its listeners and internal state alive indefinitely. This rule checks that every field with a cleanup method has a matching call in dispose(), catching missing or incomplete cleanup.
See also: State.dispose() | Dart lint: cancel_subscriptions | Dart lint: close_sinks
A form with no dispose() at all. Every controller and focus node here outlives the widget:
class _LoginFormState extends State<LoginForm> { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _emailFocus = FocusNode();
@override Widget build(BuildContext context) => const SizedBox();}class _LoginFormState extends State<LoginForm> { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); final _emailFocus = FocusNode();
@override void dispose() { _emailController.dispose(); _passwordController.dispose(); _emailFocus.dispose(); super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}A dispose() that misses one field
Section titled “A dispose() that misses one field”This is the more common shape in a live codebase: a dispose() exists, and a field added later never got a line in it. The rule reports _passwordController only.
class _LoginFormState extends State<LoginForm> { final _emailController = TextEditingController(); final _passwordController = TextEditingController(); // LINT
@override void dispose() { _emailController.dispose(); super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}The cleanup method has to be the right one
Section titled “The cleanup method has to be the right one”Each type has one expected cleanup method, and calling a different one does not count. A StreamController is closed, not disposed:
class _FeedState extends State<Feed> { final _events = StreamController<int>();
@override void dispose() { _events.close(); // `close`, not `dispose` super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}A StreamSubscription is cancelled, and a Timer is cancelled too:
class _TickerState extends State<Ticker> { StreamSubscription<int>? _subscription; Timer? _timer;
@override void dispose() { _subscription?.cancel(); _timer?.cancel(); super.dispose(); }
@override Widget build(BuildContext context) => const SizedBox();}Known limitations
Section titled “Known limitations”The cleanup call is matched by the field’s name as written. _controller.dispose() and this._controller.dispose() both count, but routing the call through a local or a helper does not:
@overridevoid dispose() { final c = _controller; c.dispose(); // still reported: the rule does not see `_controller` _disposeEverything(); // a helper's contents are not inspected super.dispose();}Calls inside a closure are not counted either, since the closure runs on its own schedule and may never run at all:
@overridevoid dispose() { WidgetsBinding.instance.addPostFrameCallback((_) { _controller.dispose(); // still reported }); super.dispose();}Both are deliberate — the rule proves cleanup happens on the direct path through dispose(), and anything else is beyond what it can confirm. Add a // ignore: many_lints/dispose_fields where a helper genuinely owns the teardown.
Static fields are skipped: they are not per-widget state and do not die with the element.
Turning this rule off
Section titled “Turning this rule off”This rule is in the core preset, so it is on with preset: core,
preset: recommended or preset: opinionated.
To turn it off:
rules: dispose_fields: falseTo keep the rule on but skip certain paths, use per-rule exclude.
Options
Section titled “Options”many_lints: rules: dispose_fields: additional_cleanup_methods: [release, shutdown]rules: dispose_fields: 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 |
state_base_classes |
list of strings | [] |
Extra base classes treated as state. Only needed for a base that does not extend Flutter’s State; an intermediate BaseState<T> is already recognised |
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 field needs cleaning up” and as “this call cleans it up”.
Related rules
Section titled “Related rules”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.dispose_provided_instances— Ensure disposable instances in Riverpod providers are cleaned up with ref.onDispose.avoid_late_final_reassignment— Flag alate finalfield assigned twice on one path.