skip to content

In Flutter, who should call dispose() on a ChangeNotifier that a ListenableBuilder listens to, and what goes wrong when the wrong object does?

level: middleimportance: must knowfreq 56%

answer

  1. creator owns, creator disposes
  2. State.dispose before super.dispose()
  3. builders remove listeners, never dispose
  4. used after being disposed
  5. never create it inside build

basics

~20 s

The 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 s

Ownership 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 lines
dart
import '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

for a junior

Remember the rule: the State that creates a notifier disposes it in dispose(), and never create one inside build().

for a middle

Explain that the builders add and remove their own listener but never dispose, and what the debug assertion after disposal tells you.

for a senior

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.

for a principal

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