skip to content

In Flutter, when does the framework call an InheritedWidget's updateShouldNotify, and what goes wrong when it compares the wrong thing?

level: middleimportance: should knowfreq 42%

answer

  1. only on a new widget instance
  2. receives the previous widget, same runtimeType
  3. true: didChangeDependencies on each dependent
  4. identity compare on a fresh object
  5. mutation in place never notifies

basics

~20 s

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

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

for a junior

Remember it runs only when a new instance replaces the old one, and that it receives the previous widget to compare against.

for a middle

Walk through updated, notifyClients and didChangeDependencies, and explain why a mutated field or a reused instance never reaches the comparison.

for a senior

Diagnose stale or over-eager rebuilds by checking which fields the comparison covers and whether the compared objects have value equality.

for a principal

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.