Sitelet https://github.com/flutter/flutter/pull/189635
Skip to content

Add service extension getSemanticsTree - #189635

Merged
auto-submit[bot] merged 12 commits into
flutter:masterfrom
hannah-hyj:getSemanticsTree
Aug 7, 2026
Merged

auto-submit[bot] merged 12 commits into
flutter:masterfrom
hannah-hyj:getSemanticsTree

Conversation

@hannah-hyj

@hannah-hyj hannah-hyj commented Jul 17, 2026 •

Copy link
Copy Markdown
Member

tracking issue: flutter/devtools#9893

Add a service extension ,it will be used to visualize semantics tree in devtool.

Pre-launch Checklist

tracking issue: flutter/devtools#9893

It will used to visualize semantics tree in devtool

  • I updated/added relevant documentation (doc comments with ///).
  • I added new tests to check the change I am making, or this PR is test-exempt.
  • I followed the breaking change policy and added Data Driven Fixes where supported.
  • All existing and new tests are passing.

If you need help, consider asking for advice on the #hackers-new channel on Discord.

If this change needs to override an active code freeze, provide a comment explaining why. The code freeze workflow can be overridden by code reviewers. See pinned issues for any active code freezes with guidance.

Note: The Flutter team is currently trialing the use of Gemini Code Assist for GitHub. Comments from the gemini-code-assist bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed.

@flutter-dashboard flutter-dashboard Bot added the CICD Run CI/CD label Jul 17, 2026
@github-actions github-actions Bot added the framework flutter/packages/flutter repository. See also f: labels. label Jul 17, 2026

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request adds a new service extension, getSemanticsTree, to the WidgetInspectorService to retrieve and serialize the semantics tree as a JSON map. Feedback suggests enhancing the serialization by including additional properties (such as tooltips, actions, and transform matrices), prioritizing the non-deprecated rootPipelineOwner in pipeline owner lookup, and utilizing ensureVisualUpdate instead of scheduleWarmUpFrame for safer frame scheduling.

Comment thread packages/flutter/lib/src/widgets/widget_inspector.dart Outdated
@elliette

Copy link
Copy Markdown
Member

Thanks for taking this on, I'm very excited for the accessibility panel in DevTools! I think my main concern is I'm not sure that adding these new service extensions to the existing WidgetInspector is the right approach. I think it will be more maintainable in the long run if the semantics/accessibility extensions are registered under a separate namespace and implemented in a separate class (maybe AccessibilityInspector or SemanticsInspector) especially since I know we are planning on adding other service extensions as well. Let me know what you think - curious what @kenzieschmoll and @chunhtai thoughts are on this as well. Thanks!

@hannah-hyj

hannah-hyj commented Jul 17, 2026 •

Copy link
Copy Markdown
Member Author

Yeah adding new AccessibilityInspector should be easier for future maintenance.
The a11y page needs more service extension like ScaleFactorOverride, boldTextOverride, highContrastOverride, etc. I was thinking about adding them to WidgetInspector or FoundationServiceExtensions (the location brightnessOverride already lives), but I thought about it again, Yyah it makes more sense if all new services for a11y reside in AccessibilityInspector.

@github-actions github-actions Bot added the a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) label Jul 17, 2026
Comment thread packages/flutter/test/widgets/widget_inspector_test.dart Outdated
Comment thread packages/flutter/lib/src/widgets/binding.dart
@kenzieschmoll

Copy link
Copy Markdown
Member

Thanks for taking this on, I'm very excited for the accessibility panel in DevTools! I think my main concern is I'm not sure that adding these new service extensions to the existing WidgetInspector is the right approach. I think it will be more maintainable in the long run if the semantics/accessibility extensions are registered under a separate namespace and implemented in a separate class (maybe AccessibilityInspector or SemanticsInspector) especially since I know we are planning on adding other service extensions as well. Let me know what you think - curious what @kenzieschmoll and @chunhtai thoughts are on this as well. Thanks!

Completely agree with @elliette.

@hannah-hyj
hannah-hyj requested a review from kenzieschmoll July 17, 2026 23:23
Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async {
_semanticsHandle ??= SemanticsBinding.instance.ensureSemantics();

PipelineOwner? findPipelineOwner() {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for having this declared as function inside _getSemanticsTree vs as a separate private method?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No particular reasons, I've moved it out as a private method for readability

};
}

final SemanticsOwner semanticsOwner = pipelineOwner.semanticsOwner!;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we guard against the case where semanticsOwner is null here? (Maybe by returning error response like above?)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added the error response

return <String, dynamic>{'error': 'rootSemanticsNode is null', 'needsFrame': true};
}

Map<String, dynamic> toJsonMap(SemanticsNode node) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider implementing toJsonMap on SemanticsNode instead

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved to SemanticsNode

Comment thread packages/flutter/lib/src/widgets/widget_inspector.dart
Comment thread packages/flutter/test/widgets/widget_inspector_test.dart

PipelineOwner? findPipelineOwner() {
for (final RenderView renderView in RendererBinding.instance.renderViews) {
if (renderView.owner?.semanticsOwner != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this is returning the first semanticsOwner of any renderView. Could there be multiple such renderViews? Maybe with multi-view?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I haven't really thought about how multi-view semantics tree UI should look like in the dev tool tab. I will support single view for now, and add a todo about multi view here.

}

Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async {
_semanticsHandle ??= SemanticsBinding.instance.ensureSemantics();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be initialized but undisposed? It is disposed only in resetAllState, which is test-only.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

Added disposeSemantics similar to widget inspector's disposeGroup call

@hannah-hyj
hannah-hyj force-pushed the getSemanticsTree branch 2 times, most recently from 62ac6f8 to 14c3c1f Compare July 28, 2026 03:21
@hannah-hyj
hannah-hyj requested a review from chunhtai July 28, 2026 04:20

@chunhtai chunhtai left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have existing vmservice for semantics tree dump, do you think we can repurpose it instead?

name: RenderingServiceExtensions.debugDumpSemanticsTreeInInverseHitTestOrder.name,

Edit: just realize this is tree dump in string representation, so i guess not then.

}

Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async {
_semanticsHandle ??= SemanticsBinding.instance.ensureSemantics();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should probably separate out the ensuresemantics to its own service extension incase we have have other use case we want to enable semantics but not get the semantics tree.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since now enablesemantics is a separate endpoint, we should just return something like semantics not enabled

Map<String, dynamic> toJsonMap({
DebugSemanticsDumpOrder childOrder = DebugSemanticsDumpOrder.traversalOrder,
}) {
final SemanticsData data = getSemanticsData();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider implement toJson method in SemanticsData directly so it will be more discoverable when adding new property.

also vmservice will call toJson on complex object directly if you throw it into the response IIRC. so there is really to have to call toJsonMap directly

}
}
final children = <Map<String, dynamic>>[
for (final SemanticsNode child in debugListChildrenInOrder(childOrder))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is in traversal order, should also find a way to return hittest order or both at the same time.

can also consider return a flat map where key is id, and the children list will just be a list of id instead of recursive object.

also the transform here is hittest transform, traversal transform is computed before addtoupdate. if we want to include both, we probably need to refactor something out

return <String, dynamic>{'error': 'rootSemanticsNode is null', 'needsFrame': true};
}

return root.toJsonMap();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should put this in a more standard format like

{'data': root} incase we want to send more thing besides the tree in the future

}
final PipelineOwner rootOwner = RendererBinding.instance.rootPipelineOwner;
if (rootOwner.semanticsOwner != null) {
return rootOwner;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

root is always empty i think

'width': rect.width,
'height': rect.height,
},
if (transform != null) 'transform': transform!.storage.toList(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not something need to be address, just note that this is hittest transform.

}

Future<Map<String, dynamic>> _getSemanticsTree(Map<String, String> parameters) async {
_semanticsHandle ??= SemanticsBinding.instance.ensureSemantics();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since now enablesemantics is a separate endpoint, we should just return something like semantics not enabled

@hannah-hyj
hannah-hyj requested a review from chunhtai July 29, 2026 22:46
Comment thread packages/flutter/lib/widgets.dart Outdated

export 'foundation.dart' show Brightness, UniqueKey;
export 'rendering.dart' show TextSelectionHandleType;
export 'src/widgets/accessibility_inspector.dart';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this being exported? I'm not sure it needs to be public.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the practice of widget_inspector.dart.
But yeah accessibility_inspector.dart doesn't really need to be exported, it doesn't define public widgets or anything, i will revert the export here

@chunhtai chunhtai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mostly lgtm, just some question about error handling

final PipelineOwner? pipelineOwner = _findPipelineOwner();
final SemanticsOwner? semanticsOwner = pipelineOwner?.semanticsOwner;
if (semanticsOwner == null) {
return <String, dynamic>{'error': 'No PipelineOwner with SemanticsOwner found'};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @bkonyi, when we want to return error, should we throw directly? looking at the registerServiceExtension, looks like it will convert error throw into a error response to the call

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really depends. If you throw, it's going to be reported as a generic extension error and also go through FlutterError.reportError. BindingBase.registerServiceExtension's implementation unfortunately takes away a lot of control over error responses.

FWIW, I haven't found other service extensions in the framework that throw or explicitly return error state. You could always consider just returning empty responses and/or add a "success" property to indicate whether or not the request was successful.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will just keep the current approach and not throw then.

@hannah-hyj
hannah-hyj requested a review from bkonyi July 30, 2026 20:36
Comment thread packages/flutter/lib/src/semantics/semantics.dart Outdated
Comment thread packages/flutter/lib/src/semantics/semantics.dart Outdated
Comment thread packages/flutter/lib/src/widgets/accessibility_inspector.dart Outdated
Comment thread packages/flutter/lib/src/widgets/accessibility_inspector.dart Outdated
final PipelineOwner? pipelineOwner = _findPipelineOwner();
final SemanticsOwner? semanticsOwner = pipelineOwner?.semanticsOwner;
if (semanticsOwner == null) {
return <String, dynamic>{'error': 'No PipelineOwner with SemanticsOwner found'};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It really depends. If you throw, it's going to be reported as a generic extension error and also go through FlutterError.reportError. BindingBase.registerServiceExtension's implementation unfortunately takes away a lot of control over error responses.

FWIW, I haven't found other service extensions in the framework that throw or explicitly return error state. You could always consider just returning empty responses and/or add a "success" property to indicate whether or not the request was successful.

Comment thread packages/flutter/lib/src/widgets/accessibility_inspector.dart Outdated
@hannah-hyj
hannah-hyj dismissed stale reviews from chunhtai and elliette via 095dc5c August 6, 2026 20:19
@hannah-hyj
hannah-hyj requested review from chunhtai and elliette August 6, 2026 20:20
elliette
elliette previously approved these changes Aug 6, 2026

@elliette elliette left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with one note about the formatting change

typedef ChildSemanticsConfigurationsDelegate = ChildSemanticsConfigurationsResult Function(
List<SemanticsConfiguration>,
);
typedef ChildSemanticsConfigurationsDelegate =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we are trying to avoid formatting changes until #187204 is resolved so that we can re-format as part of the Dart bump

@hannah-hyj hannah-hyj added the autosubmit Merge PR when tree becomes green via auto submit App label Aug 6, 2026
@auto-submit
auto-submit Bot added this pull request to the merge queue Aug 7, 2026
Merged via the queue into flutter:master with commit 198e1ce Aug 7, 2026
11 checks passed
@flutter-dashboard flutter-dashboard Bot removed the autosubmit Merge PR when tree becomes green via auto submit App label Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

a: accessibility Accessibility, e.g. VoiceOver or TalkBack. (aka a11y) CICD Run CI/CD framework flutter/packages/flutter repository. See also f: labels.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants