Repository navigation
Apply view metrics overrides below the framework - #194311
Merged
auto-submit[bot] merged 27 commits intoOct 30, 2026
Merged
Conversation
In debug mode, BindingBase.platformDispatcher is now ui.PlatformDispatcher.instance wrapped so that the entries of debugViewMetricsOverrides apply to the view metrics it and its FlutterViews report. Everything the framework reads through the binding - MediaQueryData.fromView, the RenderView's ViewConfiguration, pointer coordinate conversion, SemanticsBinding.accessibilityFeatures, image resolution, accessibility evaluations - sees the overridden values, with no override-aware code in any of those places. - debugApplyViewMetricsOverrides installs the wrapper; BindingBase applies it to the engine singleton, TestWidgetsFlutterBinding to the dispatcher its TestPlatformDispatcher wraps. Explicit test values win. - debugViewWithMetricsOverrides normalizes a raw dart:ui view to the one the binding vends; RenderView, View, RawView and MediaQueryData.fromView use it. - debugSetViewMetricsOverride and debugClearViewMetricsOverrides diff the change by group and invoke exactly the dart:ui callbacks the platform would, in dart:ui's order, in the zones they were registered in, continuing past a callback that throws. - Platform-wide metrics resolve per view through view.platformDispatcher and from the implicit view's entry on the binding's dispatcher. - A physicalSize override makes the view fixed size and is the size an unsized scene renders at; textScaling tables are interpolated; displayFeatures move with an overridden device pixel ratio. The wrappers implement the complete dart:ui interfaces with no noSuchMethod, so an engine roll that adds a member breaks this file at compile time rather than returning null to the framework. Nothing is constructed outside of debug mode. Part of flutter/devtools#9893
Moves the wrappers from _view_metrics.dart into debug.dart, next to the registry, and simplifies them: one module-level cache of view wrappers, per-view dispatchers that need no back-reference to their root, and one function that diffs an override change and announces it on every wrapped dispatcher, continuing past a failing callback. The callbacks run in the caller's zone, as TestPlatformDispatcher's own synthetic notifications do. Stops passing an overridden physical size to FlutterView.render: the engine discards frames that do not satisfy the real view constraints, so the scene is rendered at the real size instead. flutter_test wraps super.platformDispatcher instead of PlatformDispatcher.instance, and the platform's own AccessibilityFeatures object is reported unless a flag is actually overridden.
victorsanni
marked this pull request as ready for review
October 14, 2026 01:32
Contributor
There was a problem hiding this comment.
Code Review
This pull request implements the application and propagation of debugViewMetricsOverrides in debug mode by wrapping the platform dispatcher and normalizing views across RenderView, MediaQueryData, View, and RawView. The review feedback identifies a potential memory leak of WeakReference objects in _debugWrappedDispatchers and potential StateError exceptions when textScaling is an empty list in textScaleFactor and scaleFontSize.
chunhtai
reviewed
Oct 16, 2026
Review follow-ups: - The calls to debugApplyViewMetricsOverrides and debugViewWithMetricsOverrides in BindingBase, RenderView, MediaQueryData.fromView and View/RawView are behind kDebugMode, so the mode gating is visible where the helpers are used. The helpers keep their own assert gating for any other caller. - debug.dart imports dart:ui with an explicit show list again. The wrappers' implements clauses are what catch a member added to dart:ui; the import never did. - The per-group change detection moves onto DebugViewMetricsOverride as _changesFrom, which == and the replay of dart:ui callbacks on an override change now share. - The guarded reads of a view's dispatcher and id are explained at the call site, and the helper returns T? instead of a one-element record: both members are non-nullable in dart:ui. - _debugWrapView is idempotent, like _debugWrapDispatcher. - The comment on the dispatcher registry says when there is more than one wrapped dispatcher and why every one is notified.
binding.dart imports dart:ui as ui with a show list that has no FlutterView, so [ui.FlutterView.viewId] does not resolve now that the comment_references lint is enabled (flutter#194728). The unprefixed reference resolves through the file's @DocImport 'dart:ui', like the other FlutterView references in it.
The tests in service_extensions_test.dart share one binding, test each extension in one test, and run in a random order on CI. The separate end-to-end test for viewMetricsOverride left a frame scheduled and two extension events behind, so whichever tests the day's seed put after it failed: the ones that start by expecting no scheduled frame, and the viewMetricsOverride test, which counts that extension's events from zero. The end-to-end check is now part of the viewMetricsOverride test, and that test runs any frame it leaves scheduled from a tear-down, so that a failing expectation cannot leave one behind either.
debugViewWithMetricsOverrides wrapped the engine's dispatcher as a side effect, only so that the lookup on the next line would succeed for a View constructed before the binding. The view wrapper is keyed by the engine view itself, so such a View already ends up with the view the binding vends later; the engine's dispatcher now just widens the condition. Also corrects the BindingBase.platformDispatcher docs, which said TestWidgetsFlutterBinding applies debugApplyViewMetricsOverrides (it builds on super.platformDispatcher), and says on View.view and RawView.view that the normalization is debug-only, as RenderView's docs do.
chunhtai
reviewed
Oct 23, 2026
debugViewWithMetricsOverrides read platformDispatcher and viewId through a helper that treated a NoSuchMethodError or UnimplementedError as "not an engine view". A test double handed to RenderView, View, RawView or MediaQueryData.fromView now has to implement platformDispatcher, which the one in the repository that did not now does.
The same kDebugMode-gated expression RenderView and MediaQueryData.fromView use, so that nothing override-related is on the release path. The check that a deprecated RenderView renders into the normalized view is an initializer list assert again.
debugClearViewMetricsOverrides replayed the dart:ui callbacks once per removed entry. The callbacks are not per view, so the groups the entries changed are now combined and announced once.
TestPlatformDispatcher.scaleFontSize multiplied by textScaleFactor even without a test value, so a DebugViewMetricsOverride.textScaling table of more than one point reached widget tests as a linear factor. Without a test value it now asks the dispatcher it wraps, so that dispatcher's own curve applies. An explicit textScaleFactorTestValue still scales linearly. Existing tests see the same values, because the engine returns sizes unchanged at a factor of 1 and the web engine is linear. The fake dispatcher in platform_dispatcher_test now implements scaleFontSize instead of relying on the old behavior.
A textScaling table reports the ratio of its first point as textScaleFactor. When that ratio is 1, SystemTextScaler equals TextScaler.noScaling and TextStyle never asks it to scale, so a table like [(8, 8), (24, 36)] scaled no text at all. dart:ui never reports that state, because its scaleFontSize returns its input whenever textScaleFactor is 1. Both fromJson and the direct gate now reject such a table, as they reject the other tables they cannot apply. The textScaling documentation also states that text already laid out picks up a new table with the same first-point ratio only when it lays out again, because SystemTextScalers compare by their factor.
debugViewWithMetricsOverrides wrapped any view that reported the engine's dispatcher or a wrapped one. That included decorators around engine views and test doubles, so in debug mode only, View.of returned an object of a different type than the one passed to View, even with no override installed. Now only the view the dispatcher itself reports for the id is normalized, along with the deprecated window, which stands for the implicit view. The window now normalizes to the same wrapper as the implicit view instead of a separate one. Every other view is returned unchanged.
TestPlatformDispatcher.scaleFontSize used to need only textScaleFactor. Since it started asking the wrapped dispatcher, a fake dispatcher that implements textScaleFactor but not scaleFontSize threw as soon as text laid out. Such a fake now gets the linear scaling it got before, the way TestFlutterView.devicePixelRatio already handles fakes. Dispatchers that implement scaleFontSize, including the debug wrapper, still decide the scaling themselves, so text scaling tables keep reaching text. The platform_dispatcher_test fake no longer implements scaleFontSize, so its existing test covers the fallback.
In debug mode, the view these return for an engine view is a wrapper that applies debugViewMetricsOverrides. Neither the view nor its platformDispatcher is identical to what PlatformDispatcher.instance reports. State that where people look when they compare them.
chunhtai
previously approved these changes
Oct 30, 2026
The widgets test spelled out Object? Function(MediaQueryData) six times and the foundation test spelled out DebugViewMetricsOverride Function(DebugViewMetricsOverride) six times. Name them _MetricReader and _MetricChange. Most of the diff is dart format re-indenting the two map literals around their shorter types.
auto-submit Bot
pushed a commit
to flutter/packages
that referenced
this pull request
Oct 31, 2026
flutter/flutter@5e805bf...ee8b482 2026-10-31 [email protected] Roll Skia from 9bab765df357 to 607de4b035dc (1 revision) (flutter/flutter#195728) 2026-10-31 [email protected] Roll Skia from 284054bec98a to 9bab765df357 (2 revisions) (flutter/flutter#195726) 2026-10-31 [email protected] Roll Skia from 3a3f56f7ddd4 to 284054bec98a (1 revision) (flutter/flutter#195724) 2026-10-31 [email protected] Roll Skia from 466994d27b1e to 3a3f56f7ddd4 (1 revision) (flutter/flutter#195719) 2026-10-30 [email protected] Fix XcodeProjectInterpreter dependency cycle in flutter_tools (flutter/flutter#195694) 2026-10-30 [email protected] Document that Positioned also works inside an Overlay (flutter/flutter#190607) 2026-10-30 [email protected] Roll Skia from ce0b85b46db6 to 466994d27b1e (3 revisions) (flutter/flutter#195711) 2026-10-30 [email protected] Adds Vulkan sync validation test run (flutter/flutter#195596) 2026-10-30 [email protected] [Android] Let the raster thread use every core of a chip without little cores (flutter/flutter#195071) 2026-10-30 [email protected] Roll Skia from 121d04c01e2d to ce0b85b46db6 (6 revisions) (flutter/flutter#195699) 2026-10-30 [email protected] [web] Anchor platform view slots to top-left to fix RTL document layout (flutter/flutter#194601) 2026-10-30 [email protected] Update golden file testing docs (flutter/flutter#195449) 2026-10-30 [email protected] [tool] Migrate Cache.flutterRoot static state to instance configuration (flutter/flutter#190795) 2026-10-30 [email protected] Apply view metrics overrides below the framework (flutter/flutter#194311) 2026-10-30 [email protected] Roll Skia from c0c46ee9f938 to 121d04c01e2d (9 revisions) (flutter/flutter#195687) 2026-10-30 [email protected] Use uniforms in the Porter Duff blend shader instead of specialization constants (flutter/flutter#195458) 2026-10-30 [email protected] [flutter_tools] Inject dependencies into iOS, macOS, and Windows application packages (flutter/flutter#194047) If this roll has caused a breakage, revert this CL and set the roller to dry run mode using the controls here: https://autoroll.skia.org/r/flutter-packages Please CC [email protected],[email protected] on the revert to ensure that a human is aware of the problem. To file a bug in Packages: https://github.com/flutter/flutter/issues/new/choose To report a problem with the AutoRoller itself, please file a bug: https://issues.skia.org/issues/new?component=1389291&template=1850622 Documentation for the AutoRoller is here: https://skia.googlesource.com/buildbot/+doc/main/autoroll/README.md
gmackall
pushed a commit
to gmackall/flutter
that referenced
this pull request
Nov 3, 2026
Part of flutter/devtools#9893 (the DevTools accessibility page). Design: [flutter.dev/go/view-metrics-overrides](https://flutter.dev/go/view-metrics-overrides). flutter#192764 added the `debugViewMetricsOverrides` registry and the `ext.flutter.viewMetricsOverride` service extension. This PR makes those overrides take effect, which the DevTools override controls depend on. Next up: `flutter_test` support for test views with their own ids, and moving `ext.flutter.brightnessOverride` onto the registry. ## What this does In debug mode, `BindingBase.platformDispatcher` wraps `PlatformDispatcher.instance`. The wrapper and its views report overridden metrics, so everything that reads them through the binding (`MediaQuery`, `RenderView`, gestures, semantics, image resolution, accessibility evaluations) sees the overrides without any override-specific code. Changing an override fires the same `dart:ui` callbacks a real platform change would. `RenderView`, `View`, `RawView` and `MediaQueryData.fromView` map a raw engine view to the binding's wrapped one, so overrides apply to it too. Widget tests see overrides through `TestWidgetsFlutterBinding`, and explicit test values still take precedence. Profile and release builds are unchanged. New API: `debugApplyViewMetricsOverrides` wraps a dispatcher, for bindings that supply their own, and `debugViewWithMetricsOverrides` maps a raw engine view to its wrapper. ## Behavior changes and caveats - Debug only: `View.of(context)` and the binding's views are no longer `identical` to the ones `PlatformDispatcher.instance` reports. A view's `platformDispatcher` is a wrapper for that view, not `PlatformDispatcher.instance` or an app binding's dispatcher. Compare views by `viewId`. - Debug only: a `FlutterView` test double passed to `View`, `RawView`, `RenderView` or `MediaQueryData.fromView` must implement `platformDispatcher` and `viewId`. - `TestPlatformDispatcher.scaleFontSize` asks the wrapped dispatcher unless `textScaleFactorTestValue` is set, so non-linear text scaling applies in tests. A fake dispatcher without `scaleFontSize` still gets linear scaling. - An overridden `physicalSize` lays out at that size but still renders into the real surface, anchored top-left. - An ancestor `MediaQuery` still wins over platform-wide overrides, as it does over real platform settings. `debugBrightnessOverride` still wins over an overridden brightness. - `ui.window`, direct `PlatformDispatcher.instance` reads and engine-drawn content (platform views, system UI, keyboard) don't see overrides. - Text scalers compare by `textScaleFactor`, so a new `textScaling` table with the same first-point ratio only reaches already laid-out text when that text lays out again. ## Tests - `foundation/debug_view_metrics_override_test.dart`: the wrappers, per metric and per view, plus view identity, normalization, change notifications and validation. - `widgets/debug_view_metrics_override_test.dart`: end to end through `MediaQuery`, views, layout, pointer events, semantics, images and accessibility evaluations, and precedence of explicit test values. - `service_extensions_test.dart` covers an override set through the extension. `platform_dispatcher_test.dart` covers `scaleFontSize` delegation and its fallback for fakes. ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [AI contribution guidelines] and understand my responsibilities, or I am not using AI tools. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant in-code documentation (doc comments with `///`). - [x] If this PR introduces a new feature or capability, I created and linked a website documentation issue or PR in [flutter/website] (or verified none is needed). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [AI contribution guidelines]: https://github.com/flutter/flutter/blob/main/CONTRIBUTING.ai.md [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [flutter/website]: https://github.com/flutter/website [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of flutter/devtools#9893 (the DevTools accessibility page). Design: flutter.dev/go/view-metrics-overrides.
#192764 added the
debugViewMetricsOverridesregistry and theext.flutter.viewMetricsOverrideservice extension. This PR makes those overrides take effect, which the DevTools override controls depend on. Next up:flutter_testsupport for test views with their own ids, and movingext.flutter.brightnessOverrideonto the registry.What this does
In debug mode,
BindingBase.platformDispatcherwrapsPlatformDispatcher.instance. The wrapper and its views report overridden metrics, so everything that reads them through the binding (MediaQuery,RenderView, gestures, semantics, image resolution, accessibility evaluations) sees the overrides without any override-specific code. Changing an override fires the samedart:uicallbacks a real platform change would.RenderView,View,RawViewandMediaQueryData.fromViewmap a raw engine view to the binding's wrapped one, so overrides apply to it too. Widget tests see overrides throughTestWidgetsFlutterBinding, and explicit test values still take precedence. Profile and release builds are unchanged.New API:
debugApplyViewMetricsOverrideswraps a dispatcher, for bindings that supply their own, anddebugViewWithMetricsOverridesmaps a raw engine view to its wrapper.Behavior changes and caveats
View.of(context)and the binding's views are no longeridenticalto the onesPlatformDispatcher.instancereports. A view'splatformDispatcheris a wrapper for that view, notPlatformDispatcher.instanceor an app binding's dispatcher. Compare views byviewId.FlutterViewtest double passed toView,RawView,RenderVieworMediaQueryData.fromViewmust implementplatformDispatcherandviewId.TestPlatformDispatcher.scaleFontSizeasks the wrapped dispatcher unlesstextScaleFactorTestValueis set, so non-linear text scaling applies in tests. A fake dispatcher withoutscaleFontSizestill gets linear scaling.physicalSizelays out at that size but still renders into the real surface, anchored top-left.MediaQuerystill wins over platform-wide overrides, as it does over real platform settings.debugBrightnessOverridestill wins over an overridden brightness.ui.window, directPlatformDispatcher.instancereads and engine-drawn content (platform views, system UI, keyboard) don't see overrides.textScaleFactor, so a newtextScalingtable with the same first-point ratio only reaches already laid-out text when that text lays out again.Tests
foundation/debug_view_metrics_override_test.dart: the wrappers, per metric and per view, plus view identity, normalization, change notifications and validation.widgets/debug_view_metrics_override_test.dart: end to end throughMediaQuery, views, layout, pointer events, semantics, images and accessibility evaluations, and precedence of explicit test values.service_extensions_test.dartcovers an override set through the extension.platform_dispatcher_test.dartcoversscaleFontSizedelegation and its fallback for fakes.Pre-launch Checklist
///).If you need help, consider asking for advice on the #hackers-new channel on Discord.