In Flutter, how do you build an InheritedModel so a widget that reads only the unit system does not rebuild when the map style changes?
answer
- InheritedModel<T>, T is the aspect type
- inheritFrom(context, aspect: ...)
- updateShouldNotify gates first
- updateShouldNotifyDependent(oldWidget, Set<T>)
- null aspect means depend on everything
basics
~10 sExtend InheritedModel<Aspect>, expose accessors that call InheritedModel.inheritFrom with an aspect, and override updateShouldNotify plus updateShouldNotifyDependent, which receives each dependent's aspect set and returns true only if a field for one of those aspects changed.
solid answer
~40 sDeclare `class TrailSettingsModel extends InheritedModel<TrailAspect>` holding `units` and `mapStyle`, and give it accessors such as `unitsOf(context)` that call `InheritedModel.inheritFrom<TrailSettingsModel>(context, aspect: TrailAspect.units)`. The element records a **set of aspects** per dependent. When the model is rebuilt, `updateShouldNotify` is the first gate: if it returns `false`, nobody is checked. If `true`, `updateShouldNotifyDependent(oldWidget, dependencies)` runs for each dependent and should return `true` only when a field matching one of its aspects changed. A dependent registered without an aspect has an empty set, meaning "everything", and rebuilds unconditionally. `isSupportedAspect` lets a nested model shadow some aspects and pass others to an ancestor model of the same type. It is worth it only when the parts change independently and have many readers.
code
dart · 39 linesimport 'package:flutter/widgets.dart';
enum UnitSystem { metric, imperial }
enum MapStyle { topo, satellite }
enum TrailAspect { units, mapStyle }
class TrailSettingsModel extends InheritedModel<TrailAspect> {
const TrailSettingsModel({
super.key,
required this.units,
required this.mapStyle,
required super.child,
});
final UnitSystem units;
final MapStyle mapStyle;
static UnitSystem unitsOf(BuildContext context) =>
InheritedModel.inheritFrom<TrailSettingsModel>(context, aspect: TrailAspect.units)!.units;
static MapStyle mapStyleOf(BuildContext context) =>
InheritedModel.inheritFrom<TrailSettingsModel>(context, aspect: TrailAspect.mapStyle)!
.mapStyle;
@override
bool updateShouldNotify(TrailSettingsModel oldWidget) =>
units != oldWidget.units || mapStyle != oldWidget.mapStyle;
@override
bool updateShouldNotifyDependent(
TrailSettingsModel oldWidget,
Set<TrailAspect> dependencies,
) {
return (dependencies.contains(TrailAspect.units) && units != oldWidget.units) ||
(dependencies.contains(TrailAspect.mapStyle) && mapStyle != oldWidget.mapStyle);
}
}go deeper
Know that InheritedModel exists to let widgets depend on one part of shared data, and that MediaQuery.sizeOf is built on it.
Explain inheritFrom with an aspect, the per-dependent aspect set, and the two-stage check of updateShouldNotify then updateShouldNotifyDependent.
Implement both comparisons correctly, avoid stray no-aspect lookups that make a dependency unconditional, and justify the model over two plain scopes.
Judge whether aspect-level rebuild control is worth its correctness risk compared with splitting scopes or adopting a selector-based state library.
## The problem InheritedModel solves A plain `InheritedWidget` has one notify decision for all dependents. A hiking app's settings scope might hold the **unit system** (read by dozens of distance, elevation and pace labels) and the **map style** (read by the map and its legend). With a plain scope, switching the map style from topographic to satellite rebuilds every distance label, because `updateShouldNotify` can only say "something changed". **`InheritedModel<T>`** is an `InheritedWidget` whose dependents qualify their dependency with an **aspect** of type `T`. The model then decides per dependent. ## The pieces 1. **The aspect type.** Usually an enum, such as `enum TrailAspect { units, mapStyle }`. `MediaQuery` uses a private enum the same way. 2. **Registering accessors.** `InheritedModel.inheritFrom<M>(context, aspect: a)` finds the model and records `a` for the calling element. Wrap it in static methods so callers never pass raw aspects. 3. **`updateShouldNotify(oldWidget)`**, the coarse gate: return `true` if anything any dependent could read changed. 4. **`updateShouldNotifyDependent(oldWidget, dependencies)`**, the fine gate: `dependencies` is that dependent's `Set<T>` of aspects; return `true` only if a changed field corresponds to one of them. 5. **`isSupportedAspect(aspect)`**, optional, defaulting to `true`. ## What the element does The `InheritedModelElement` keeps, per dependent, either nothing, an empty set, or a set of aspects: | Registration | Stored as | Notified when | |---|---|---| | `inheritFrom(context)` with no aspect | empty set | `updateShouldNotify` is true | | `inheritFrom(context, aspect: units)` | `{units}` | gate true and dependent check true for `{units}` | | a no-aspect call plus an aspect call, in either order | an empty set | `updateShouldNotify` is true | Aspects **accumulate**: calling `unitsOf` and `mapStyleOf` from one build yields `{units, mapStyle}`. An unconditional registration is never narrowed afterwards; the element's dependencies are only reset when it is deactivated. That is why one stray no-aspect lookup undoes the optimisation for that element. Order of evaluation matters for correctness: if `updateShouldNotify` forgets a field, no dependent is checked at all, even if `updateShouldNotifyDependent` handles it perfectly. ## Walking through one change The app holds `units: metric` and `mapStyle: topo`. Forty distance labels called `unitsOf`, and the map and its legend called `mapStyleOf`. The user switches the map to satellite: 1. The settings `State` rebuilds `TrailSettingsModel` with `mapStyle: satellite`. 2. `updateShouldNotify` sees `mapStyle` differ and returns `true`. 3. For each distance label, `updateShouldNotifyDependent(old, {units})` checks `units`, finds it unchanged, and returns `false`; the label is left alone. 4. For the map and legend, the call with `{mapStyle}` returns `true`, so they get `didChangeDependencies` and rebuild. Two widgets rebuild instead of forty-two. Had one label also called a no-aspect accessor, it would have rebuilt too, because its set would be empty. ## Shadowing with `isSupportedAspect` When models of the same type are nested, `inheritFrom` starts at the nearest one. If that model's `isSupportedAspect(aspect)` returns `false`, the lookup registers on it **and** continues to the next ancestor model, stopping at the first that supports the aspect, whose data is returned. A trail-detail screen could override only `mapStyle` for its subtree while `units` still resolves to the app-wide model, and a dependent is still notified if the inner model later changes. ## When it is worth it - **Use it** when a scope holds parts that change independently, each read by many widgets, and profiling shows rebuilds of the wrong readers. - **Skip it** for a single value, for data that changes rarely, or when splitting into two separate `InheritedWidget`s is simpler; two scopes give the same isolation without aspect bookkeeping. - Remember the cost: every notification runs a per-dependent comparison, and a wrong comparison produces stale UI that is hard to spot. The framework's best-known use is `MediaQuery`, whose accessors such as `sizeOf` and `paddingOf` are exactly this pattern.
- Your model's updateShouldNotifyDependent handles mapStyle correctly, but map widgets still never update when the style changes. What do you check first?`updateShouldNotify`. It is evaluated first, and if it returns `false` the per-dependent check is never reached. A comparison that covers `units` but not `mapStyle` blocks every map-style notification regardless of how good the dependent-level logic is.
- When is splitting settings into two separate InheritedWidgets better than one InheritedModel?When the parts are unrelated and are owned or changed by different code. Two scopes give the same rebuild isolation with plain `updateShouldNotify` overrides and no aspect bookkeeping. `InheritedModel` earns its place when the parts belong together as one value, as MediaQuery's metrics do, or when there are many aspects.
saying these in an interview costs you the question
- updateShouldNotifyDependent runs even when updateShouldNotify returns false.
- A widget can depend on only one aspect of a model per build.
- Calling an aspect accessor later narrows an earlier no-aspect dependency.
- An InheritedModel automatically detects which fields each widget read.
- isSupportedAspect filters which dependents get notified.