skip to content

In Flutter, how does ValueNotifier differ from ChangeNotifier, and why can a ValueNotifier<List<String>> fail to rebuild its ValueListenableBuilder?

level: middleimportance: should knowfreq 50%

answer

  1. one value, public setter
  2. notifies only when == says different
  3. in-place list mutation is invisible
  4. assign a new list instead
  5. builder receives (context, value, child)

basics

~20 s

ValueNotifier is a ChangeNotifier holding one value; assigning value notifies only when the new value is not == to the old. Mutating a list in place changes nothing the setter can see, so ValueListenableBuilder never rebuilds.

solid answer

~40 s

`ValueNotifier<T>` extends `ChangeNotifier` and implements `ValueListenable<T>`: it wraps a single `value`, and its setter calls `notifyListeners()` only when the new value is not `==` to the old one. `ValueListenableBuilder<T>` listens to it and passes the current value to `builder(context, value, child)`. With `ValueNotifier<List<String>>`, `tags.value.add('baroque')` mutates the same list object, so the setter never runs and nothing is notified; `tags.value = tags.value` is also ignored because the list equals itself. Fix it by assigning a new list, `tags.value = [...tags.value, 'baroque']`, or use a `ChangeNotifier` subclass that owns the list and calls `notifyListeners()` itself. `ValueNotifier` fits single immutable values; `ChangeNotifier` fits models with several fields or mutable collections.

code

dart · 13 lines
dart
import 'package:flutter/foundation.dart';

final tags = ValueNotifier<List<String>>(<String>[]);

void addTagBroken(String tag) {
  // Same List object: the setter never runs, listeners are not called.
  tags.value.add(tag);
}

void addTagFixed(String tag) {
  // New List object: not == the old one, so listeners are called.
  tags.value = [...tags.value, tag];
}

go deeper

for a junior

Know that ValueNotifier holds one value, that assigning value notifies, and that ValueListenableBuilder passes that value into its builder.

for a middle

Explain the == check in the setter and why in-place mutation of a list or object held by a ValueNotifier never reaches the listeners.

for a senior

Show you design state as immutable values or switch to a ChangeNotifier subclass for mutable collections, and expose ValueListenable rather than the writable notifier.

for a principal

Weigh the cost of immutable-value discipline across a codebase against explicit notifyListeners() calls, and set a team convention that prevents silent non-notifications.

## Two classes, one mechanism **`ChangeNotifier`** is the general base: it keeps a list of listeners and exposes a `@protected` `notifyListeners()` that a subclass calls whenever *anything* it owns changes. You decide when to notify. **`ValueNotifier<T>`** is a ready-made subclass for the common case of a single value. It extends `ChangeNotifier`, implements **`ValueListenable<T>`** (an interface that `Animation<T>` also implements), and holds one field behind a public `value` getter and setter. The setter is the whole policy: ```dart set value(T newValue) { if (_value == newValue) { return; } _value = newValue; notifyListeners(); } ``` So a `ValueNotifier` decides *for you* when to notify: on assignment, and only when `==` says the new value differs. | | `ChangeNotifier` subclass | `ValueNotifier<T>` | |---|---|---| | State shape | any fields you declare | exactly one `value` | | Who notifies | your methods call `notifyListeners()` | the `value` setter | | Notification rule | whenever you call it | only if the new value is not `==` the old | | Needs a subclass | yes | no, use it directly | | Typical use | a view model, a cart-like model | a flag, a counter, a progress fraction | ## ValueListenableBuilder **`ValueListenableBuilder<T>`** takes `valueListenable`, `builder` and an optional `child`. Its builder has the signature `ValueWidgetBuilder<T>`: `Widget Function(BuildContext context, T value, Widget? child)`. Its state reads `valueListenable.value` in `initState`, adds a listener, and on each notification calls `setState` to store the fresh value and rebuild. Compared with `ListenableBuilder`, the only difference is convenience: the value arrives as a typed argument. ## Why the list case fails The trap is that **equality, not mutation, drives notification**: 1. `final tags = ValueNotifier<List<String>>([]);` 2. `tags.value.add('baroque');` calls the getter, gets the same `List` object and mutates it. The setter never runs, so no one is notified. 3. `tags.value = tags.value;` does run the setter, but a Dart `List` uses identity for `==`, the object equals itself, and the setter returns early. The builder therefore keeps showing the old list until something unrelated rebuilds it, which makes the bug look intermittent. The same rule bites any mutable object: changing a field on an object held by a `ValueNotifier` is invisible. It also works the other way: a class that overrides `==` to compare by content will suppress notifications when you assign a new but equal instance, which is usually what you want. ## Fixes - **Assign a new value**: `tags.value = [...tags.value, 'baroque'];` A new list is not identical, so it notifies. - **Keep values immutable**: records, `final` fields with `copyWith`, or unmodifiable lists make "assign a new value" the only way to change state. - **Switch to `ChangeNotifier`** when the state is a mutable collection or several related fields; its methods mutate and then call `notifyListeners()` explicitly. - Avoid calling `tags.notifyListeners()` from outside: the method is `@protected` and `@visibleForTesting`, so the analyzer flags it, and it hides the design problem. ## Choosing between them - A single scalar such as a selected tab index, a toggle, or a download fraction: `ValueNotifier` plus `ValueListenableBuilder`. - A model with behaviour and several fields that change together: a `ChangeNotifier` subclass plus `ListenableBuilder`. - Several independent `ValueNotifier`s that one widget needs: `ListenableBuilder` with `Listenable.merge`, reading each `.value` inside the builder. Both notifiers still need an owner that calls `dispose()`.

  • In Flutter, what happens if you assign a ValueNotifier the same value it already holds?
    Nothing. The `value` setter compares with `==` and returns early when the values are equal, so listeners are not called and no builder rebuilds. For a class that overrides `==` by content, assigning an equal new instance is also ignored.
  • In Flutter, why does ValueListenableBuilder accept a ValueListenable rather than a ValueNotifier?
    `ValueListenable<T>` is the read-only interface with just `value` plus the listener methods. Both `ValueNotifier<T>` and `Animation<T>` implement it, so the builder works with either, and a model can expose a `ValueListenable` to widgets while keeping the writable `ValueNotifier` private.

saying these in an interview costs you the question

  • ValueNotifier notifies whenever its value is mutated in place
  • Reassigning the same list instance forces a ValueNotifier to notify
  • ValueNotifier is unrelated to ChangeNotifier and needs no disposal
  • Calling notifyListeners() on a ValueNotifier from a widget is the proper fix
  • ValueListenableBuilder only works with ValueNotifier, not animations