With the provider package, a Selector or context.select either never rebuilds or rebuilds on every notification; what are the usual causes and fixes?
answer
- what counts as changed
- same mutated list compares equal
- new object without == never equal
- new Selector instance resets the cache
- itemBuilder context is the whole list
basics
~20 sA selector returning a list mutated in place compares equal to itself, so nothing rebuilds; one returning a new object without == is never equal, so everything rebuilds. Return values or records; a parent rebuild also reruns Selector's builder.
solid answer
~40 sBoth widgets rebuild when the previous and next selected values are unequal under `DeepCollectionEquality`, or when `Selector`'s `shouldRebuild` says so. If the selector returns the notifier's internal `List` and `add()` mutates it in place, the remembered value and the new one are the same object, so they compare equal and the builder never runs; return `List.unmodifiable(_items)`, a length, or an immutable snapshot. If it returns a new class instance without an `==` override, identity comparison means it always differs; return a Dart 3 record like `(b.count, b.total)` or override `==`. `Selector` also re-runs its builder whenever its parent rebuilds with a new `Selector` instance, so it filters only provider-driven rebuilds. And `context.select` on a `ListView.builder` `itemBuilder` context asserts, because the whole list would become the dependent.
code
dart · 29 linesimport 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Basket extends ChangeNotifier {
final List<String> _skus = [];
double _total = 0;
// A fresh unmodifiable copy per call: deep comparison sees content changes.
List<String> get skus => List.unmodifiable(_skus);
int get count => _skus.length;
double get total => _total;
void add(String sku, double price) {
_skus.add(sku);
_total += price;
notifyListeners();
}
}
class BasketSummaryLine extends StatelessWidget {
const BasketSummaryLine({super.key});
@override
Widget build(BuildContext context) {
// A record has structural ==, so equal (count, total) pairs do not rebuild.
final (count, total) = context.select((Basket b) => (b.count, b.total));
return Text('$count items, ${total.toStringAsFixed(2)}');
}
}go deeper
Remember that a selector should return a plain value, like a count, rather than the mutable object the model keeps changing.
Explain DeepCollectionEquality: collections by content, other objects by ==, and why that makes identity-only classes rebuild every time.
Diagnose the three production cases (in-place mutation, identity equality, a rebuilding parent) from rebuild counts, and fix each without scattering shouldRebuild everywhere.
Push the fix into the model: immutable state with value equality makes every selector in the app correct by default instead of by review.
## Two symptoms, a few causes `Selector` and `context.select` rebuild when the value the selector returns 'changes', and in provider 6 'changes' means: the previous and next results are **not equal under `DeepCollectionEquality`** from package:collection, or, for `Selector` only, `shouldRebuild(previous, next)` returns `true`. Nearly every bug is a mismatch between that definition and the model's real shape. | Symptom | Usual cause | Fix | |---|---|---| | Never rebuilds | selector returns a mutable object the notifier mutates in place | return a copy, a scalar, or an immutable snapshot | | Rebuilds on every notification | selector creates a new object that has no `==` | return a record, a collection, or a class with `==` | | Builder runs although the value is equal | the parent rebuilt and created a new `Selector` instance | stabilise the parent, or move the Selector | | Assertion inside a list | `context.select` on `itemBuilder`'s context | wrap the item in `Builder` or extract an item widget | ## Never rebuilds: the in-place mutation ```dart class Basket extends ChangeNotifier { final List<String> items = []; void add(String sku) { items.add(sku); // same List object, mutated notifyListeners(); } } Selector<Basket, List<String>>( selector: (_, b) => b.items, builder: (_, items, __) => Text('${items.length}'), ) ``` `b.items` returns the **same list object** every time. After `add`, the selector returns that object again and compares it with the value remembered from the last build, which is that same object, already mutated. Comparing an object with itself reports 'equal', so the builder is skipped and the UI goes stale. The package documentation warns about exactly this: the selected value must be immutable, or `Selector` may think nothing changed. Fixes: - expose `List.unmodifiable(_items)`, a fresh copy per call that deep comparison checks by content; - select a scalar such as `b.items.length`; - make the model immutable and replace the list on every change. ## Rebuilds every time: a new object without == A selector returning `BasketSummary(count: b.count, total: b.total)` creates a **new instance** on every call. `BasketSummary` is not a collection, so the comparison falls back to `==`, which without an override is **identity**: never equal. The builder runs on every notification and the selector buys nothing. Options: 1. return a **record**, `(b.count, b.total)`, which has structural `==` in Dart 3; it replaces the `tuple` package the provider docs still suggest; 2. return a `List` or `Map` and rely on deep comparison; 3. override `==` and `hashCode` on the class, by hand or with a code generator. ## Builder runs although the value is equal `Selector`'s state caches the last built widget and reuses it only when the widget configuration is the **same object** as last time **and** the value compares equal. When the Selector's parent rebuilds and constructs a new `Selector(...)`, the cache is invalidated and `builder` runs regardless of the value; the package's test suite pins this down ('calls builder if the callback changes'). So `Selector` filters **provider-driven** rebuilds, not parent-driven ones. Keeping that parent stable is general rebuild-scope work. ## The list assertion ```dart ListView.builder( itemCount: ids.length, itemBuilder: (context, index) { final item = context.select((Basket b) => b.lineAt(index)); // asserts return Text(item.name); }, ) ``` The `context` handed to `itemBuilder` belongs to the list's sliver, not to an item, so `select` would make the **whole list** the dependent and rebuild every visible item when any one changes. provider asserts with 'Tried to use context.select inside a SliverList/SliderGridView.' (the typo is the package's). Wrap each item in a `Builder`, or extract an item widget, so the item's own context subscribes. ## Other rules provider enforces - `context.select` runs only while building (a `build` method or a `LayoutBuilder` builder); in `initState`, `didChangeDependencies` or a tap handler it asserts. - Inside a `context.select` callback, calling `context.read`, `watch` or `select` asserts: 'Cannot call context.read/watch/select inside the callback of a context.select'. - A selector should be cheap and free of side effects: for `context.select` it runs on every notification for each dependent that is not already marked dirty. - Deep comparison of a large collection costs time on each notification; with an immutable model, `shouldRebuild: (a, b) => !identical(a, b)` on a `Selector` turns it into an identity check. ## Diagnosing in practice Flutter DevTools' rebuild counts, or a temporary `debugPrint` in the builder, show which case you are in: a builder that never fires after a change points at in-place mutation; one that fires on unrelated changes points at identity equality or a rebuilding parent.
- Why is a Dart 3 record a good return type for a multi-value selector?Records have structural equality: `(3, 9.5) == (3, 9.5)` is true even for two separate instances. So `context.select((Basket b) => (b.count, b.total))` rebuilds only when one of the fields changes, without writing a class with `==` or depending on the `tuple` package the provider docs mention.
- A Selector's builder runs on every scroll frame although the basket is untouched. Where do you look?At the Selector's parent. `Selector` reuses its cached widget only when it receives the same widget instance and an equal value; a parent that rebuilds each frame creates a new `Selector` every time, which invalidates the cache. Move the Selector out of the rebuilding subtree or stop the parent's rebuilds.
saying these in an interview costs you the question
- DeepCollectionEquality catches in-place mutation because it compares contents
- Selector only ever rebuilds when the selected value changes
- Returning a new object from the selector is fine because provider compares fields
- context.select inside itemBuilder rebuilds just that item
- shouldRebuild is consulted on the very first build