In Flutter, what is a ChangeNotifier, and how does a ListenableBuilder rebuild when that notifier calls notifyListeners()?
answer
- state object outside the widget tree
- Listenable: addListener and removeListener
- mutate, then notifyListeners()
- builder subscribes in initState
- only the builder's subtree rebuilds
basics
~10 sChangeNotifier is a foundation class that holds mutable state outside widgets and calls its registered listeners when notifyListeners() runs. ListenableBuilder subscribes to it and calls setState internally, so only the builder's subtree rebuilds.
solid answer
~40 s`ChangeNotifier` (in `package:flutter/foundation.dart`) implements `Listenable`: it keeps a list of `VoidCallback` listeners, and a subclass changes its fields and then calls `notifyListeners()` to invoke them. It is a `mixin class`, so you can `extends` or `with` it. `ListenableBuilder(listenable: model, builder: (context, child) => ...)` adds a listener when it is inserted, removes it when it is disposed, and on each notification marks itself dirty, so the next frame re-runs only its `builder`. The widget that owns the screen does not rebuild, which is the point: the state lives in a plain Dart object, testable without widgets, and the rebuild is scoped to the part that reads it. The notifier's owner must still call `dispose()` on it.
code
dart · 25 linesimport 'package:flutter/material.dart';
class ScoreLibrary extends ChangeNotifier {
final List<String> _titles = [];
List<String> get titles => List.unmodifiable(_titles);
void add(String title) {
_titles.add(title);
notifyListeners();
}
}
class LibraryCount extends StatelessWidget {
const LibraryCount({super.key, required this.library});
final ScoreLibrary library;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: library,
builder: (context, child) => Text('${library.titles.length} scores'),
);
}
}go deeper
Recall that ChangeNotifier lives in Flutter's foundation library, that a model calls notifyListeners() after changing its fields, and that ListenableBuilder rebuilds only its builder's output.
Explain the subscription lifecycle: the builder adds its listener in initState, swaps it in didUpdateWidget and removes it in dispose, and calls setState with an empty body because the state lives in the notifier.
Show you scope builders tightly around the widgets that read the model, keep mutations behind methods, and decide explicitly which object owns and disposes each notifier.
Argue when plain ChangeNotifier view models are enough for a team and when the missing structure (dependency injection, async state, disposal rules) justifies adopting a state library instead.
## What a ChangeNotifier is `ChangeNotifier` is a small class in Flutter's **foundation** library (`package:flutter/foundation.dart`), not in the widgets layer. It implements the **`Listenable`** interface, which has only two methods: - `addListener(VoidCallback listener)` registers a zero-argument callback. - `removeListener(VoidCallback listener)` unregisters it. `ChangeNotifier` adds the machinery behind those methods: a list of listeners, a `@protected` method **`notifyListeners()`** that calls each of them, `hasListeners`, and **`dispose()`**, which clears the list and makes the object unusable. Since Dart 3 it is declared as a **`mixin class`**, so a model can either `extends ChangeNotifier` or mix it in with `with ChangeNotifier` when it already has a superclass. The notifier knows nothing about widgets. It is a plain Dart object holding state *outside* the widget tree, so it survives rebuilds, can be shared by several screens, and can be unit-tested without pumping a widget. ## The notify cycle A typical model keeps private fields, exposes read-only getters, and changes state only through methods that end with `notifyListeners()`: ```dart class ScoreLibrary extends ChangeNotifier { final List<String> _titles = []; List<String> get titles => List.unmodifiable(_titles); void add(String title) { _titles.add(title); notifyListeners(); } } ``` The steps when `add` runs: 1. The field changes. 2. `notifyListeners()` calls every registered listener, synchronously, in registration order. 3. Each listening widget marks itself dirty. 4. On the next frame, the framework re-runs the `build` of each dirty widget. If the method forgets `notifyListeners()`, the data changes but no widget hears about it: the classic "the value updated but the screen did not" bug. ## How ListenableBuilder connects it to the UI **`ListenableBuilder`** is the general widget for rebuilding part of a tree from any `Listenable`. Its constructor takes `listenable`, a `builder` of type `TransitionBuilder` (`(BuildContext context, Widget? child)`), and an optional `child`. Internally it is an `AnimatedWidget`. Its state: - calls `listenable.addListener(_handleChange)` in `initState`; - in `didUpdateWidget`, if a *different* listenable is passed, removes the listener from the old one and adds it to the new one; - removes the listener in `dispose`; - in `_handleChange`, returns early if it is no longer mounted, otherwise calls `setState` with an empty body, because the listenable already holds the new state. Only the `ListenableBuilder`'s element is marked dirty, so only its `builder` output is rebuilt. The enclosing screen, its `Scaffold` and siblings are untouched. ```dart ListenableBuilder( listenable: library, builder: (context, child) => Text('${library.titles.length} scores'), ) ``` ## Where it sits among the alternatives | Tool | Holds state in | Rebuild scope | |---|---|---| | `setState` in a `State` | the `State` object | that widget's whole `build` | | `ChangeNotifier` + `ListenableBuilder` | a separate Dart object | only the builder's subtree | | `ValueNotifier` + `ValueListenableBuilder` | a single value in a notifier | only the builder's subtree | Flutter's own app-architecture guide uses exactly this pairing: view models extend `ChangeNotifier` and views wrap the parts that read them in `ListenableBuilder`. State libraries build on the same idea with more structure around it. ## Common mistakes - Mutating a public field from outside and expecting a rebuild; only `notifyListeners()` triggers one. - Calling `notifyListeners()` from outside the class; it is `@protected`, so the analyzer warns. - Wrapping the whole screen in the builder, which throws away the scoping benefit. - Creating the notifier inside `build`, so every rebuild gets a fresh, empty model. - Forgetting that whoever creates the notifier must call `dispose()` when done. The mental model to carry into an interview: the notifier is a **publisher of "something changed"**, it carries no data in the callback, and the builder re-reads the model's getters when it rebuilds.
- In Flutter, does notifyListeners() pass the changed data to listeners?No. A `ChangeNotifier` listener is a `VoidCallback` with no arguments. It only signals that something changed; the listener, or the builder when it rebuilds, reads the new state from the model's getters. `ValueListenableBuilder` looks as if it receives the value, but it reads `valueListenable.value` itself when notified.
- In Flutter, can a class that already extends another class still use ChangeNotifier?Yes. `ChangeNotifier` is declared as a `mixin class`, so it can be used with `with ChangeNotifier` on a class that extends something else, as well as with `extends ChangeNotifier`. Either way the class gains `addListener`, `removeListener`, `notifyListeners` and `dispose`.
A ChangeNotifier works like a departure board operator who rings a bell when anything changes. The bell says nothing about what changed; only people standing at that board look up and re-read it, and the rest of the station carries on.
saying these in an interview costs you the question
- Changing a notifier's field rebuilds widgets even without notifyListeners()
- ChangeNotifier is part of the provider package rather than Flutter
- ListenableBuilder rebuilds the whole enclosing screen on every notification
- The listener callback receives the new value as its argument
- A ChangeNotifier must be created inside the build method that reads it