In Flutter, who should call dispose() on a ChangeNotifier that a ListenableBuilder listens to, and what goes wrong when the wrong object does?
answer
- creator owns, creator disposes
- State.dispose before super.dispose()
- builders remove listeners, never dispose
- used after being disposed
- never create it inside build
basics
~20 sThe object that created the notifier disposes it, typically a State that created it in a field or initState and disposes it in its own dispose(). ListenableBuilder only removes its listener; disposing a notifier you were handed breaks every other listener.
solid answer
~40 sOwnership follows creation. If a `State` creates a `ValueNotifier` or `ChangeNotifier` subclass, it calls `notifier.dispose()` in its own `dispose()` before `super.dispose()`. `ListenableBuilder` and `ValueListenableBuilder` never dispose what they listen to: they add a listener when inserted and remove it in their own `dispose`. A widget that receives a notifier through its constructor must not dispose it, because the owner and other listeners still use it; after `dispose()`, `addListener` or `notifyListeners()` hit a debug assertion that the notifier "was used after being disposed". Creating the notifier inside `build` is the other classic bug: every rebuild makes a new, empty instance that nobody disposes. Subclasses that hold timers or subscriptions override `dispose()` and must call `super.dispose()`.
code
dart · 34 linesimport 'package:flutter/material.dart';
class ScorePage extends StatefulWidget {
const ScorePage({super.key});
@override
State<ScorePage> createState() => _ScorePageState();
}
class _ScorePageState extends State<ScorePage> {
final ValueNotifier<int> _page = ValueNotifier<int>(1);
@override
void dispose() {
_page.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ValueListenableBuilder<int>(
valueListenable: _page,
builder: (context, page, child) => Text('Page $page'),
),
TextButton(
onPressed: () => _page.value++,
child: const Text('Next page'),
),
],
);
}
}go deeper
Remember the rule: the State that creates a notifier disposes it in dispose(), and never create one inside build().
Explain that the builders add and remove their own listener but never dispose, and what the debug assertion after disposal tells you.
Diagnose leaks from manual addListener calls on long-lived notifiers, and make ownership explicit in constructor APIs so no widget disposes what it was handed.
Set a codebase convention for notifier ownership and disposal that survives many contributors, and decide where a library's scoped lifetime management earns its dependency.
## The ownership rule A `ChangeNotifier` has a lifecycle: it is created, it gathers listeners, and eventually someone calls **`dispose()`**. `dispose()` clears the listener list and marks the object unusable; in debug builds any later `addListener`, `notifyListeners()` or second `dispose()` fails an assertion with the message that the notifier *was used after being disposed*. `removeListener` is deliberately allowed after disposal, so late listeners can still clean up. The framework does not decide who disposes. The convention, and what interviewers probe, is simple: **whoever creates the notifier owns it and disposes it.** Everyone else only listens. ## Who does what | Party | Adds a listener | Removes it | Calls `dispose()` | |---|---|---|---| | `ListenableBuilder` / `ValueListenableBuilder` | in `initState` | in its own `dispose`, or when a new listenable arrives | never | | A `State` that created the notifier | maybe, manually | if it added one | yes, in `State.dispose` | | A widget that received it via constructor | maybe | yes, if it added one | no | | A longer-lived service object | n/a | n/a | when the service itself is torn down | The correct shape for a screen-scoped notifier: ```dart class _ScorePageState extends State<ScorePage> { final ValueNotifier<int> _page = ValueNotifier<int>(1); @override void dispose() { _page.dispose(); super.dispose(); } // build() wraps the page label in ValueListenableBuilder(valueListenable: _page, ...) } ``` ## What goes wrong 1. **Created inside `build`.** `ValueListenableBuilder(valueListenable: ValueNotifier(0), ...)` builds a new notifier on every rebuild. The builder sees a different listenable in `didUpdateWidget`, re-subscribes, and its state resets to the new initial value; none of the discarded instances is disposed. 2. **Disposed by a non-owner.** A child widget that disposes a notifier passed in from its parent leaves the parent and siblings holding a dead object. Its existing listeners were cleared, so they silently stop updating, and new subscriptions or notifications fail an assertion in debug builds. 3. **Disposed too early.** Disposing in `deactivate` is premature, because a deactivated `State` can be reinserted in the same frame; disposing while the notifier is in the middle of `notifyListeners()` has its own assertion, because it would modify the listener list during iteration. 4. **Never disposed.** For a notifier owned by a `State` whose listeners are all builders, the builders already removed their listeners, so the practical damage is small, but leak-tracking tools flag it and any resources the subclass holds stay open. The serious leak is a **long-lived notifier** (a service or singleton) to which a widget added a listener manually with `addListener` and never removed it: the closure keeps the widget's `State` alive. ## Subclasses that own resources A `ChangeNotifier` subclass that starts a `Timer`, a stream subscription or a controller must override `dispose()`, release those, and call `super.dispose()`. The base method is annotated `@mustCallSuper`, so the analyzer warns if the call is missing. ## Practical checklist - Create screen-scoped notifiers as `State` fields or in `initState`, never in `build`. - Dispose them in `State.dispose()`, before `super.dispose()`. - Accept externally owned notifiers as constructor parameters and never dispose them. - Pair every manual `addListener` with a `removeListener` in the same widget's `dispose`. - Let state libraries own disposal where they create the notifier for you. The interview answer in one line: builders listen, owners dispose, and the owner is whoever called the constructor.
- In Flutter, may you call removeListener on a ChangeNotifier that was already disposed?Yes. `removeListener` returns without error on a disposed notifier, because owners are often disposed a frame before their listeners. `addListener`, `notifyListeners()` and a second `dispose()` are the calls that assert in debug mode.
- In Flutter, why is it a bug to write ValueListenableBuilder(valueListenable: ValueNotifier(0), ...) inside build()?Each rebuild constructs a new notifier. The builder's `didUpdateWidget` sees a different listenable, swaps its listener and resets its value to 0, so any state is lost; and no code ever disposes the old instances. Create the notifier once in the `State` instead.
- In Flutter, what must a ChangeNotifier subclass that owns a Timer do in dispose()?Cancel the timer and release any other resources, then call `super.dispose()`. The base `dispose()` is `@mustCallSuper`, and skipping the super call leaves the listener list intact and the debug disposal flag unset.
saying these in an interview costs you the question
- ListenableBuilder disposes its listenable when it leaves the tree
- A widget should dispose every notifier passed into its constructor
- Creating a ValueNotifier inside build() is fine because it is cheap
- dispose() notifies listeners one final time before clearing them
- An undisposed notifier always leaks, even when nothing else references it