From 0fab43a5338da301d4e37ccaa942e68cd4cb93de Mon Sep 17 00:00:00 2001 From: mbcorona Date: Mon, 27 Jul 2026 09:44:46 -0600 Subject: [PATCH] [web] Dont crash when a platform view is disposed mid-frame --- .../src/engine/platform_views/embedder.dart | 45 ++++++++++++++-- .../web_ui/test/ui/platform_view_test.dart | 53 +++++++++++++++++++ 2 files changed, 93 insertions(+), 5 deletions(-) diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart index dddbb02efac5e..80ba7d30a11e8 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_views/embedder.dart @@ -517,6 +517,10 @@ class PlatformViewEmbedder { canvas.displayCanvas!.setIsOverlay(needsOverlayPositioning); } + // Platform views which were disposed while they were still being + // composited. They are left out of the DOM. + final missingViewIds = {}; + // At this point, the DOM contains the static elements and the elements from // the previous composition which need to move. We iterate over the static // elements and insert the elements which come before them into the DOM. @@ -524,16 +528,27 @@ class PlatformViewEmbedder { var nextCompositionIndex = 0; while (staticElementIndex < staticElements.length) { final int staticElementIndexInActiveComposition = staticElements[staticElementIndex]; - final DomElement staticDomElement = _getElement( + final DomElement? staticDomElement = _getElement( _activeComposition.entities[staticElementIndexInActiveComposition], ); + if (staticDomElement == null) { + // This view is not in the DOM, so it can't position other elements. + // Use the next static element instead. + staticElementIndex++; + continue; + } // Go through next composition elements until we reach the static element. while (indexMap[nextCompositionIndex] != staticElementIndexInActiveComposition) { final CompositionEntity nextEntity = composition.entities[nextCompositionIndex]; if (nextEntity is CompositionCanvas) { updateCompositionCanvasWithDisplay(nextEntity, nextCompositionIndex); } - sceneHost.insertBefore(_getElement(nextEntity), staticDomElement); + final DomElement? nextDomElement = _getElement(nextEntity); + if (nextDomElement != null) { + sceneHost.insertBefore(nextDomElement, staticDomElement); + } else if (nextEntity is CompositionPlatformView) { + missingViewIds.add(nextEntity.viewId); + } nextCompositionIndex++; } if (composition.entities[nextCompositionIndex] is CompositionCanvas) { @@ -554,15 +569,35 @@ class PlatformViewEmbedder { if (nextEntity is CompositionCanvas) { updateCompositionCanvasWithDisplay(nextEntity, nextCompositionIndex); } - sceneHost.append(_getElement(nextEntity)); + final DomElement? nextDomElement = _getElement(nextEntity); + if (nextDomElement != null) { + sceneHost.append(nextDomElement); + } else if (nextEntity is CompositionPlatformView) { + missingViewIds.add(nextEntity.viewId); + } nextCompositionIndex++; } + + if (missingViewIds.isNotEmpty) { + // Drop the skipped views, so the next frame doesn't expect them to be + // in the DOM. + composition.entities.removeWhere( + (CompositionEntity entity) => + entity is CompositionPlatformView && missingViewIds.contains(entity.viewId), + ); + printWarning( + 'Cannot render platform views: ${missingViewIds.join(', ')}. ' + 'These views were disposed while they were being composited.', + ); + } } - DomElement _getElement(CompositionEntity entity) { + /// Returns the DOM element for [entity], or null if the platform view no + /// longer has a clip chain because it was disposed. + DomElement? _getElement(CompositionEntity entity) { return switch (entity) { CompositionCanvas() => entity.displayCanvas!.hostElement, - CompositionPlatformView() => _viewClipChains[entity.viewId]!.root, + CompositionPlatformView() => _viewClipChains[entity.viewId]?.root, }; } diff --git a/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart b/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart index 518902f38ced1..00de1727c6845 100644 --- a/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart +++ b/engine/src/flutter/lib/web_ui/test/ui/platform_view_test.dart @@ -824,6 +824,59 @@ Future testMain() async { expect(platformViewsHost.querySelector('flt-platform-view'), isNull); }); + test('does not crash when a platform view is disposed mid-frame', () async { + await createPlatformView(0, platformViewType); + await createPlatformView(1, platformViewType); + + final sb = ui.SceneBuilder() + ..pushOffset(0, 0) + ..addPlatformView(0, width: 10, height: 10) + ..addPlatformView(1, width: 10, height: 10) + ..pop(); + await renderScene(sb.build()); + _expectSceneMatches(<_EmbeddedViewMarker>[_platformView, _platformView]); + + // Build the next frame by hand so that a platform view can be disposed + // after the composition was created, but before the frame is submitted. + final ViewRasterizer rasterizer = renderer.rasterizers[implicitView.viewId]!; + final PlatformViewEmbedder embedder = rasterizer.viewEmbedder; + final rootLayer = RootLayer(); + // The views are composited in the opposite order, so the DOM needs to be + // updated for this composition. + rootLayer.children.add(PlatformViewLayer(1, ui.Offset.zero, 10, 10)); + rootLayer.children.add(PlatformViewLayer(0, ui.Offset.zero, 10, 10)); + + embedder.frameSize = rasterizer.currentFrameSize; + final Frame frame = rasterizer.context.acquireFrame(embedder); + frame.raster(LayerTree(rootLayer), rasterizer.currentFrameSize, null); + + // View 0 is disposed while it is still part of the composition which is + // about to be submitted. + embedder.disposeView(0); + + final warnings = []; + final void Function(String) originalPrintWarning = printWarning; + printWarning = (String warning) => warnings.add(warning); + try { + await expectLater(embedder.submitFrame(null), completes); + } finally { + printWarning = originalPrintWarning; + } + + // The disposed view is left out of the composition. + _expectSceneMatches(<_EmbeddedViewMarker>[_platformView]); + expect(embedder.debugActiveComposition.entities, hasLength(1)); + expect( + warnings, + contains( + contains( + 'Cannot render platform views: 0. ' + 'These views were disposed while they were being composited.', + ), + ), + ); + }); + test('preserves the DOM node of an unrendered platform view', () async { await createPlatformView(1, platformViewType);