In Flutter, when does the framework call an InheritedWidget's updateShouldNotify, and what goes wrong when it compares the wrong thing?
answer
- only on a new widget instance
- receives the previous widget, same runtimeType
- true: didChangeDependencies on each dependent
- identity compare on a fresh object
- mutation in place never notifies
basics
~20 supdateShouldNotify runs only when the InheritedWidget's position receives a new widget instance; it gets the old widget and returns whether dependents must rebuild. Comparing too loosely misses changes; comparing a freshly built object by identity rebuilds every dependent every time.
solid answer
~40 sThe framework calls `updateShouldNotify(oldWidget)` when the parent rebuilds and hands the `InheritedElement` a **new** widget instance of the same type. If it returns `true`, the element calls `didChangeDependencies` on every registered dependent; if `false`, dependents are left alone, though the child is still updated through the normal parent-to-child path. It is never called when the same instance is reused, and never when someone mutates an object the widget holds. Two classic bugs follow: comparing by **identity** a value that is rebuilt as a new object each time (a fresh settings object or list) notifies on every rebuild, and comparing only **part** of the data (checking `units` but not `mapStyle`) leaves dependents showing stale values.
code
dart · 51 linesimport 'package:flutter/widgets.dart';
enum UnitSystem { metric, imperial }
enum MapStyle { topo, satellite }
class TrailPrefsScope extends InheritedWidget {
const TrailPrefsScope({
super.key,
required this.units,
required this.mapStyle,
required super.child,
});
final UnitSystem units;
final MapStyle mapStyle;
static TrailPrefsScope of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<TrailPrefsScope>()!;
// Compare every field a dependent reads; checking only `units`
// would leave map widgets stale after a style change.
@override
bool updateShouldNotify(TrailPrefsScope oldWidget) =>
units != oldWidget.units || mapStyle != oldWidget.mapStyle;
}
class TrailPrefsHost extends StatefulWidget {
const TrailPrefsHost({super.key, required this.child});
final Widget child;
@override
State<TrailPrefsHost> createState() => _TrailPrefsHostState();
}
class _TrailPrefsHostState extends State<TrailPrefsHost> {
UnitSystem _units = UnitSystem.metric;
final MapStyle _mapStyle = MapStyle.topo;
void toggleUnits() => setState(() {
_units = _units == UnitSystem.metric ? UnitSystem.imperial : UnitSystem.metric;
});
@override
Widget build(BuildContext context) {
// widget.child is the same instance on every rebuild, so only
// registered dependents rebuild when a preference changes.
return TrailPrefsScope(units: _units, mapStyle: _mapStyle, child: widget.child);
}
}go deeper
Remember it runs only when a new instance replaces the old one, and that it receives the previous widget to compare against.
Walk through updated, notifyClients and didChangeDependencies, and explain why a mutated field or a reused instance never reaches the comparison.
Diagnose stale or over-eager rebuilds by checking which fields the comparison covers and whether the compared objects have value equality.
Set a team rule for scoped data: immutable value types with equality, or a Listenable-based scope, so reviewers stop chasing notify bugs.
## When the method runs An `InheritedWidget` is a `ProxyWidget`: its element, an **`InheritedElement`**, simply builds the `child` it was given. When the widget above rebuilds and returns a new `InheritedWidget` instance at the same position, the framework's `update` path runs: 1. The element swaps in the new widget. 2. It calls `updated(oldWidget)`, and the `InheritedElement` override asks `widget.updateShouldNotify(oldWidget)`. 3. If that returns `true`, it calls `notifyClients`, which visits every registered **dependent** and calls its `didChangeDependencies`, marking it dirty. 4. It rebuilds itself, which updates the `child` through the normal path. Two cases never reach step 2: - **The same instance is reused.** If the parent returns an identical (for example `const`) widget, the child update short-circuits and nothing is compared. - **Something inside the widget is mutated.** A field on a mutable object changes without any widget being built, so the framework has nothing to compare. The argument is the **previous widget**, and the framework guarantees it has the same `runtimeType` as `this`, which is why the override can use a covariant parameter type such as `UnitsScope oldWidget`. ## What `true` and `false` actually mean | Return | Registered dependents | The `child` subtree | |---|---|---| | `true` | Each gets `didChangeDependencies` and rebuilds; a `State` sees `didChangeDependencies` before `build` | Updated as usual; unchanged instances are skipped | | `false` | Left alone, keep showing what they built last | Updated as usual; unchanged instances are skipped | The return value only gates the dependents that exist outside the parent's normal rebuild path. If the scope's parent passes a brand-new child every time, that child rebuilds regardless; the typical design keeps `child` as a stable instance so dependents are the only widgets that rebuild. ## Bug 1: identity comparison on a fresh object A scope that bundles settings into an object and compares with `!=` relies on that object's `==`. If the class does not override `==`, identity is used, and a parent that builds `TrailSettings(units: _units, mapStyle: _style)` in every `build` produces a new object each time. Every rebuild of the scope then notifies every dependent, even when no setting changed. The fixes are to give the data **value equality** (override `==` and `hashCode`, or hold the fields directly on the widget and compare them) or to keep the object stable and replace it only when a setting changes. ## Bug 2: comparing only part of the data If the scope later gains a `mapStyle` field but `updateShouldNotify` still checks only `units`, changing the map style returns `false`. Dependents keep rendering the old style until something else rebuilds them. That bug is intermittent, which makes it hard to trace, because any unrelated rebuild hides it. ## Bug 3: returning `true` unconditionally It is correct but wasteful: every time the owning `State` calls `setState` for any reason, all dependents rebuild. On a scope near the root, that can mean much of the app. ## Bug 4: mutating in place Storing a mutable object and changing a field does nothing visible. A write such as `settings.units = UnitSystem.imperial` builds no widget, so there is no `oldWidget` and no comparison. Either rebuild the scope with a new immutable value, or use `InheritedNotifier`, which listens to a `Listenable` and notifies when it fires. ## Diagnosing a notify bug When a dependent shows a stale value, or rebuilds far too often, work through it in order: 1. Put a breakpoint or a debug print in `updateShouldNotify` and change the setting. If it is never hit, no new scope instance is being built: look for in-place mutation, or for a parent that reuses the same scope instance. 2. If it is hit but returns `false`, compare the fields it checks against the fields the stale dependent actually reads. 3. If it returns `true` on every rebuild, check whether an object field is compared by identity and rebuilt each time. 4. Confirm the stale widget really registered: a widget that reads the data through a non-registering lookup, or through a value captured once and cached in its own state, is never notified whatever the scope returns. Most real bugs are caught at step 1 or 2. ## Checklist for a correct override - Compare **every** field that a dependent reads. - Use value equality for any object field. - Keep the scope's `child` a stable instance supplied from above. - Do not put side effects in `updateShouldNotify`; it is a pure comparison run during the build phase.
- If updateShouldNotify returns false, can a dependent still rebuild in that same frame?Yes. `false` only means the scope does not mark its dependents dirty. A dependent still rebuilds if its own parent rebuilds and passes it a new widget instance, or if its own `State` calls `setState`. That is why a missing field in the comparison can hide behind unrelated rebuilds and show up only intermittently.
- How do you make a changing value notify dependents without rebuilding the scope from above?Use `InheritedNotifier` with a `Listenable` such as a `ValueNotifier`. Its element listens to the notifier and, when it fires, marks itself dirty and notifies dependents during its next build, without a new scope instance. The scope's `updateShouldNotify` then only handles the case where the notifier object itself is swapped.
saying these in an interview costs you the question
- updateShouldNotify runs whenever any field inside the scoped object changes.
- Returning false stops the scope's whole child subtree from rebuilding.
- The oldWidget argument may be a different InheritedWidget type.
- Comparing a freshly built settings object with != is always safe.
- Returning true always is harmless because Flutter skips unchanged widgets.