In Flutter, why does calling MediaQuery.of(context) rebuild a widget more often than expected, and how do accessors like MediaQuery.sizeOf fix it?
answer
- MediaQuery is an InheritedModel
- of registers without an aspect
- keyboard moves viewInsets
- sizeOf, widthOf, paddingOf register one aspect
- Theme.of has no aspects
basics
~20 sMediaQuery.of registers an unconditional dependency, so the widget rebuilds whenever any MediaQueryData field changes, such as keyboard insets. MediaQuery is an InheritedModel; sizeOf, widthOf and paddingOf register one aspect and rebuild only when that value changes.
solid answer
~40 s`MediaQuery` extends `InheritedModel`, and its `MediaQueryData` bundles size, padding, `viewInsets`, `textScaler`, `platformBrightness` and more. `MediaQuery.of(context)` depends on it **without an aspect**, so the widget rebuilds when **any** field changes: the keyboard opening changes `viewInsets` over many frames, so a widget that only wanted the screen width rebuilds throughout that animation. The aspect accessors (`MediaQuery.sizeOf`, `widthOf`, `heightOf` since 3.35, `paddingOf`, `viewInsetsOf`, `textScalerOf`, `platformBrightnessOf` and others) register a single aspect, and `updateShouldNotifyDependent` rebuilds that widget only when its aspect changed. One catch: once a context has called `MediaQuery.of`, its dependency stays unconditional until the element is deactivated, so mixing `of` and `sizeOf` in one build gains nothing. `Theme.of` has no aspect accessors: `ColorScheme.of` and `TextTheme.of` call `Theme.of` and depend on the whole theme.
code
dart · 19 linesimport 'package:flutter/material.dart';
class TrailCardGrid extends StatelessWidget {
const TrailCardGrid({super.key, required this.cards});
final List<Widget> cards;
@override
Widget build(BuildContext context) {
// Rebuilds on ANY MediaQueryData change, including every keyboard frame:
// final double width = MediaQuery.of(context).size.width;
// Rebuilds only when the width changes:
final double width = MediaQuery.widthOf(context);
final int columns = width >= 600 ? 2 : 1;
return GridView.count(crossAxisCount: columns, children: cards);
}
}go deeper
Recall that MediaQuery.sizeOf and its siblings exist and are preferred over reading one field from MediaQuery.of.
Explain that MediaQuery is an InheritedModel, that of registers no aspect, and that updateShouldNotifyDependent compares only the fields for the registered aspects.
Trace a rebuild storm during keyboard animation to a MediaQuery.of call, fix it with the narrow accessor, and check no other call in that element keeps the dependency unconditional.
Make narrow MediaQuery accessors a lint-backed convention and decide where coarse theme dependencies are acceptable given how rarely themes change.
## What MediaQuery carries `MediaQuery` is the widget that exposes the current view's metrics as a **`MediaQueryData`** value: `size`, `devicePixelRatio`, `padding`, `viewPadding`, `viewInsets`, `systemGestureInsets`, `textScaler`, `platformBrightness`, accessibility flags such as `disableAnimations` and `boldText`, and more. The framework inserts one near the root of each view, and it is rebuilt whenever the platform reports new metrics. Several of those fields change often: - **`viewInsets`** typically changes over many frames while the software keyboard animates open or closed; - **`size`** and orientation change on rotation, window resize, split screen or foldable posture; - **`padding`** changes when system bars appear or hide; - `platformBrightness` and `textScaler` change when the user edits system settings. ## Why `MediaQuery.of` over-rebuilds Since Flutter 3.10, `MediaQuery` extends **`InheritedModel`**, an `InheritedWidget` whose dependents can name the **aspect** they care about. `MediaQuery.of(context)` calls `InheritedModel.inheritFrom` with **no aspect**, which is the same as `dependOnInheritedWidgetOfExactType`: an unconditional dependency. `MediaQuery.updateShouldNotify` returns `true` whenever the new `MediaQueryData` differs from the old in any field, and an unconditional dependent is rebuilt every time that happens. So a hiking app's trail card that does `MediaQuery.of(context).size.width` to pick a one- or two-column layout also rebuilds on every keyboard animation frame when the user types in the trail search field, and on every status-bar change. ## How the aspect accessors narrow it Each accessor passes a private aspect value to `inheritFrom`: | Accessor | Rebuilds the caller when | |---|---| | `MediaQuery.of(context)` | any `MediaQueryData` field differs | | `MediaQuery.sizeOf(context)` | `size` changes | | `MediaQuery.widthOf` / `heightOf` (3.35+) | only that dimension changes | | `MediaQuery.paddingOf(context)` | `padding` changes | | `MediaQuery.viewInsetsOf(context)` | `viewInsets` changes | | `MediaQuery.textScalerOf(context)` | `textScaler` changes | | `MediaQuery.platformBrightnessOf(context)` | `platformBrightness` changes | The `InheritedModelElement` stores a **set of aspects** per dependent. When the model notifies, it calls `updateShouldNotifyDependent(oldWidget, aspects)` for each dependent, and `MediaQuery`'s override checks only the fields matching those aspects. A widget that called `widthOf` is untouched by a keyboard animation, and also by a height-only change. Each accessor has a nullable `maybe...Of` twin for code that may run without a `MediaQuery` above it. ## Worked example: the trail search screen The trail search screen has a search field at the top and a grid of trail cards below. Each card chooses one or two columns from the screen width and pads itself away from the display cutout. - **Before:** each card calls `MediaQuery.of(context)` once and reads `size.width` and `padding`. When the user taps the search field, the keyboard animates in, `viewInsets.bottom` grows over many frames, and every card rebuilds on each of those frames even though neither value it reads changed. - **After:** each card calls `MediaQuery.widthOf(context)` and `MediaQuery.paddingOf(context)`. Its aspect set is `{width, padding}`, and the keyboard's `viewInsets` changes no longer reach it. The widget that must avoid the keyboard, such as the bottom action bar, calls `MediaQuery.viewInsetsOf(context)` and is the only one that rebuilds while the keyboard moves. The fix is a one-line change per call site, which is why reviewers flag `MediaQuery.of(context).something` on sight. ## Two traps 1. **Dependencies are sticky within an element.** An element's registered dependencies are not cleared when it rebuilds; they are removed when it is deactivated. For a model, a dependency without an aspect is recorded as an empty set meaning "everything", and later aspect calls do not narrow it. So a build method that calls `MediaQuery.of(context).padding` once and `MediaQuery.sizeOf(context)` elsewhere is still unconditionally dependent. 2. **Deprecated accessors.** `MediaQuery.textScaleFactorOf` still exists but is deprecated in favour of `textScalerOf`, which returns a `TextScaler` able to express nonlinear scaling. ## How `Theme.of` compares `Theme.of(context)` is not an `InheritedModel`. It calls `dependOnInheritedWidgetOfExactType` on the private inherited theme widget, whose `updateShouldNotify` compares the whole `ThemeData`, and it also registers on the `Localizations` scope (to localize typography) and on `InheritedCupertinoTheme`. The conveniences `ColorScheme.of(context)` and `TextTheme.of(context)` simply call `Theme.of` and read one field, so they rebuild on any theme change exactly like `Theme.of`. In practice theme changes are rare (a light/dark switch), so this matters far less than MediaQuery. ## Practical rules - Prefer the narrowest `MediaQuery` accessor for the value you read. - Never call `MediaQuery.of` in the same element as an aspect accessor if you want the narrowing. - For layout that depends on the space a parent gives rather than the screen, the answer is a different tool, owned by the responsive-layout topic.
- A build method calls MediaQuery.sizeOf and, in one branch, MediaQuery.of(context).padding. Is the widget still narrowed to size changes?No. The call without an aspect records an unconditional dependency for that element, and the model never narrows it again while the element stays active. From then on the widget rebuilds on any `MediaQueryData` change. Replace the second call with `MediaQuery.paddingOf(context)` so both dependencies are aspect-qualified.
- Does ColorScheme.of(context) narrow rebuilds to colour-scheme changes the way MediaQuery.sizeOf narrows to size?No. `ColorScheme.of` is a convenience that calls `Theme.of(context).colorScheme`. `Theme.of` depends on the inherited theme widget as a whole, whose notify rule compares the entire `ThemeData`, so any theme change rebuilds the caller. Theme has no aspect mechanism.
saying these in an interview costs you the question
- MediaQuery.of and MediaQuery.sizeOf rebuild the widget equally often.
- Calling sizeOf later in the same build narrows an earlier MediaQuery.of dependency.
- ColorScheme.of only rebuilds when the colour scheme changes.
- MediaQuery.textScaleFactorOf is the current way to read text scaling.
- Aspect accessors copy the data once and never update the widget.