[Widget Inspector] Handle null exceptions calling renderObject (#163642)

Fixes https://github.com/flutter/devtools/issues/8905

Based on the stacktrace in
https://github.com/flutter/devtools/issues/8905:

* This call to `renderObject` can throw a null-exception:
39b4951f8f/packages/flutter/lib/src/widgets/framework.dart (L3745)
* That exception is thrown here:
39b4951f8f/packages/flutter/lib/src/widgets/framework.dart (L6534)

I've been unable to figure out a way to get into a state that reproduces
this. Instead, this PR simply handles the exception and returns `null`
(because we already gracefully handle the case where the `renderObject`
is `null`.
This commit is contained in:
Elliott Brooks 2025-03-07 13:21:55 -08:00 committed by GitHub
parent c0e6f90652
commit b7bea22ab8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 38 additions and 6 deletions

View File

@ -2068,7 +2068,7 @@ mixin WidgetInspectorService {
}
final Object? value = node.value;
if (value is Element) {
final RenderObject? renderObject = value.renderObject;
final RenderObject? renderObject = _renderObjectOrNull(value);
if (renderObject is RenderParagraph) {
additionalPropertiesJson['textPreview'] = renderObject.text.toPlainText();
}
@ -2160,7 +2160,7 @@ mixin WidgetInspectorService {
return null;
}
final RenderObject? renderObject =
object is Element ? object.renderObject : (object as RenderObject?);
object is Element ? _renderObjectOrNull(object) : (object as RenderObject?);
if (renderObject == null || !renderObject.attached) {
return null;
}
@ -2224,7 +2224,7 @@ mixin WidgetInspectorService {
InspectorSerializationDelegate delegate,
) {
final Object? value = node.value;
final RenderObject? renderObject = value is Element ? value.renderObject : null;
final RenderObject? renderObject = value is Element ? _renderObjectOrNull(value) : null;
if (renderObject == null) {
return const <String, Object>{};
}
@ -2320,7 +2320,7 @@ mixin WidgetInspectorService {
final Object? object = toObject(id);
bool succeed = false;
if (object != null && object is Element) {
final RenderObject? render = object.renderObject;
final RenderObject? render = _renderObjectOrNull(object);
final ParentData? parentData = render?.parentData;
if (parentData is FlexParentData) {
parentData.fit = flexFit;
@ -2338,7 +2338,7 @@ mixin WidgetInspectorService {
final dynamic object = toObject(id);
bool succeed = false;
if (object != null && object is Element) {
final RenderObject? render = object.renderObject;
final RenderObject? render = _renderObjectOrNull(object);
final ParentData? parentData = render?.parentData;
if (parentData is FlexParentData) {
parentData.flex = factor;
@ -2362,7 +2362,7 @@ mixin WidgetInspectorService {
final Object? object = toObject(id);
bool succeed = false;
if (object != null && object is Element) {
final RenderObject? render = object.renderObject;
final RenderObject? render = _renderObjectOrNull(object);
if (render is RenderFlex) {
render.mainAxisAlignment = mainAxisAlignment;
render.crossAxisAlignment = crossAxisAlignment;
@ -2543,6 +2543,12 @@ mixin WidgetInspectorService {
_clearStats();
_resetErrorCount();
}
/// Safely get the render object of an [Element].
///
/// If the element is not yet mounted, the result will be null.
RenderObject? _renderObjectOrNull(Element element) =>
element.mounted ? element.renderObject : null;
}
/// Accumulator for a count associated with a specific source location.

View File

@ -4837,6 +4837,32 @@ class _TestWidgetInspectorService extends TestWidgetInspectorService {
expect(parentData['offsetY'], equals('293.0'));
});
testWidgets(
'ext.flutter.inspector.getLayoutExplorerNode does not throw for unmounted widget',
(WidgetTester tester) async {
// Mount the Row widget.
await pumpWidgetForLayoutExplorer(tester);
// Get the id of the Row widget.
final Element rowElement = tester.element(find.byType(Row));
service.setSelection(rowElement, group);
final String id = service.toId(rowElement, group)!;
// Unmount the Row widget.
await tester.pumpWidget(const Placeholder());
// Verify that the call to getLayoutExplorerNode for the Row widget
// does not throw an exception.
expect(
() => service.testExtension(
WidgetInspectorServiceExtensions.getLayoutExplorerNode.name,
<String, String>{'id': id, 'groupName': group, 'subtreeDepth': '1'},
),
returnsNormally,
);
},
);
testWidgets('ext.flutter.inspector.getLayoutExplorerNode for RenderBox with FlexParentData', (
WidgetTester tester,
) async {