Skip to content

Commit 3837195

Browse files
authored
[web] Defer injection of platform views until needed. (flutter#48960)
This PR defers the injection of the contents of a Platform View into the DOM of the page, until the Platform View is really needed by a renderer. This effectively means that a `platformView` will be injected into the DOM the first time that its `slot` is injected into the Shadow DOM of the Flutter web app. This makes passing a `(flutter)ViewId` parameter from the framework unnecessary, even in a multi-view app. The only cases in which this change might be breaking is those where an app tries to locate the just-created Platform View by looking into the DOM from the [`onPlatformViewCreated` callback](https://api.flutter.dev/flutter/widgets/HtmlElementView/onPlatformViewCreated.html). In those cases, [a fix like this](flutter/packages#5660) is needed (use the **only [documented way](https://api.flutter.dev/flutter/dart-ui_web/PlatformViewRegistry/getViewById.html)** to obtain the Platform View contents from its `viewId`) ## Issues Fixes flutter#137287 Closes flutter#136548 [C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
1 parent 673bdf3 commit 3837195

12 files changed

Lines changed: 166 additions & 437 deletions

File tree

lib/web_ui/lib/src/engine/canvaskit/embedded_views.dart

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,9 @@ class HtmlViewEmbedder {
118118
/// If this returns a [CkCanvas], then that canvas should be the new leaf
119119
/// node. Otherwise, keep the same leaf node.
120120
CkCanvas? compositeEmbeddedView(int viewId) {
121+
// Ensure platform view with `viewId` is injected into the `rasterizer.view`.
122+
rasterizer.view.dom.injectPlatformView(viewId);
123+
121124
final int overlayIndex = _context.visibleViewCount;
122125
_compositionOrder.add(viewId);
123126
// Keep track of the number of visible platform views.
@@ -142,10 +145,10 @@ class HtmlViewEmbedder {
142145
return recorderToUseForRendering?.recordingCanvas;
143146
}
144147

145-
void _compositeWithParams(int viewId, EmbeddedViewParams params) {
148+
void _compositeWithParams(int platformViewId, EmbeddedViewParams params) {
146149
// If we haven't seen this viewId yet, cache it for clips/transforms.
147-
final ViewClipChain clipChain = _viewClipChains.putIfAbsent(viewId, () {
148-
return ViewClipChain(view: createPlatformViewSlot(viewId));
150+
final ViewClipChain clipChain = _viewClipChains.putIfAbsent(platformViewId, () {
151+
return ViewClipChain(view: createPlatformViewSlot(platformViewId));
149152
});
150153

151154
final DomElement slot = clipChain.slot;
@@ -175,7 +178,7 @@ class HtmlViewEmbedder {
175178
}
176179

177180
// Apply mutators to the slot
178-
_applyMutators(params, slot, viewId);
181+
_applyMutators(params, slot, platformViewId);
179182
}
180183

181184
int _countClips(MutatorsStack mutators) {

lib/web_ui/lib/src/engine/dom.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,10 @@ extension DomElementExtension on DomElement {
604604
external DomElement? _querySelector(JSString selectors);
605605
DomElement? querySelector(String selectors) => _querySelector(selectors.toJS);
606606

607+
@JS('matches')
608+
external JSBoolean _matches(JSString selectors);
609+
bool matches(String selectors) => _matches(selectors.toJS).toDart;
610+
607611
@JS('querySelectorAll')
608612
external _DomList _querySelectorAll(JSString selectors);
609613
Iterable<DomElement> querySelectorAll(String selectors) =>

lib/web_ui/lib/src/engine/html/platform_view.dart

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,29 @@
33
// found in the LICENSE file.
44

55
import '../dom.dart';
6+
import '../platform_dispatcher.dart';
67
import '../platform_views/slots.dart';
8+
import '../window.dart';
79
import 'surface.dart';
810

911
/// A surface containing a platform view, which is an HTML element.
1012
class PersistedPlatformView extends PersistedLeafSurface {
11-
PersistedPlatformView(this.viewId, this.dx, this.dy, this.width, this.height);
13+
PersistedPlatformView(this.platformViewId, this.dx, this.dy, this.width, this.height) {
14+
// Ensure platform view with `viewId` is injected into the `implicitView`
15+
// before rendering its shadow DOM `slot`.
16+
final EngineFlutterView implicitView = EnginePlatformDispatcher.instance.implicitView!;
17+
implicitView.dom.injectPlatformView(platformViewId);
18+
}
1219

13-
final int viewId;
20+
final int platformViewId;
1421
final double dx;
1522
final double dy;
1623
final double width;
1724
final double height;
1825

1926
@override
2027
DomElement createElement() {
21-
return createPlatformViewSlot(viewId);
28+
return createPlatformViewSlot(platformViewId);
2229
}
2330

2431
@override
@@ -36,21 +43,21 @@ class PersistedPlatformView extends PersistedLeafSurface {
3643
bool canUpdateAsMatch(PersistedSurface oldSurface) {
3744
if (super.canUpdateAsMatch(oldSurface)) {
3845
// super checks the runtimeType of the surface, so we can just cast...
39-
return viewId == ((oldSurface as PersistedPlatformView).viewId);
46+
return platformViewId == ((oldSurface as PersistedPlatformView).platformViewId);
4047
}
4148
return false;
4249
}
4350

4451
@override
4552
double matchForUpdate(PersistedPlatformView existingSurface) {
46-
return existingSurface.viewId == viewId ? 0.0 : 1.0;
53+
return existingSurface.platformViewId == platformViewId ? 0.0 : 1.0;
4754
}
4855

4956
@override
5057
void update(PersistedPlatformView oldSurface) {
5158
assert(
52-
viewId == oldSurface.viewId,
53-
'PersistedPlatformView with different viewId should never be updated. Check the canUpdateAsMatch method.',
59+
platformViewId == oldSurface.platformViewId,
60+
'PersistedPlatformView with different platformViewId should never be updated. Check the canUpdateAsMatch method.',
5461
);
5562
super.update(oldSurface);
5663
// Only update if the view has been resized

lib/web_ui/lib/src/engine/platform_dispatcher.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
7878
_addLocaleChangedListener();
7979
registerHotRestartListener(dispose);
8080
_setAppLifecycleState(ui.AppLifecycleState.resumed);
81+
viewManager.onViewDisposed.listen((_) {
82+
// Send a metrics changed event to the framework when a view is disposed.
83+
// View creation/resize is handled by the `_didResize` handler in the
84+
// EngineFlutterView itself.
85+
invokeOnMetricsChanged();
86+
});
8187
}
8288

8389
/// The [EnginePlatformDispatcher] singleton.
@@ -623,18 +629,12 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
623629
_handleWebTestEnd2EndMessage(jsonCodec, data)));
624630
return;
625631

626-
case 'flutter/platform_views':
632+
case PlatformViewMessageHandler.channelName:
633+
// `arguments` can be a Map<String, Object> for `create`,
634+
// but an `int` for `dispose`, hence why `dynamic` everywhere.
627635
final MethodCall(:String method, :dynamic arguments) =
628636
standardCodec.decodeMethodCall(data);
629-
final int? flutterViewId = tryViewId(arguments);
630-
if (flutterViewId == null) {
631-
implicitView!.platformViewMessageHandler
632-
.handleLegacyPlatformViewCall(method, arguments, callback!);
633-
return;
634-
}
635-
arguments as Map<dynamic, dynamic>;
636-
viewManager[flutterViewId]!
637-
.platformViewMessageHandler
637+
PlatformViewMessageHandler.instance
638638
.handlePlatformViewCall(method, arguments, callback!);
639639
return;
640640

lib/web_ui/lib/src/engine/platform_views/content_manager.dart

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,7 @@ class PlatformViewManager {
3838

3939
/// The shared instance of PlatformViewManager shared across the engine to handle
4040
/// rendering of PlatformViews into the web app.
41-
// TODO(dit): How to make this overridable from tests?
42-
static final PlatformViewManager instance = PlatformViewManager();
41+
static PlatformViewManager instance = PlatformViewManager();
4342

4443
// The factory functions, indexed by the viewType
4544
final Map<String, Function> _factories = <String, Function>{};
@@ -65,6 +64,20 @@ class PlatformViewManager {
6564
return _contents.containsKey(viewId);
6665
}
6766

67+
/// Returns the cached contents of [viewId], to be injected into the DOM.
68+
///
69+
/// This is only used by the active `Renderer` object when a platform view needs
70+
/// to be injected in the DOM, through `FlutterView.DomManager.injectPlatformView`.
71+
///
72+
/// This may return null, if [renderContent] was not called before this. The
73+
/// framework seems to allow/need this for some tests, so it is allowed here
74+
/// as well.
75+
///
76+
/// App programmers should not access this directly, and instead use [getViewById].
77+
DomElement? getSlottedContent(int viewId) {
78+
return _contents[viewId];
79+
}
80+
6881
/// Returns the HTML element created by a registered factory for [viewId].
6982
///
7083
/// Throws an [AssertionError] if [viewId] hasn't been rendered before.
@@ -104,9 +117,8 @@ class PlatformViewManager {
104117

105118
/// Creates the HTML markup for the `contents` of a Platform View.
106119
///
107-
/// The result of this call is cached in the `_contents` Map. This is only
108-
/// cached so it can be disposed of later by [clearPlatformView]. _Note that
109-
/// there's no `getContents` function in this class._
120+
/// The result of this call is cached in the `_contents` Map, so the active
121+
/// renderer can inject it as needed.
110122
///
111123
/// The resulting DOM for the `contents` of a Platform View looks like this:
112124
///

lib/web_ui/lib/src/engine/platform_views/message_handler.dart

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,35 +21,41 @@ typedef PlatformViewContentHandler = void Function(DomElement);
2121

2222
/// This class handles incoming framework messages to create/dispose Platform Views.
2323
///
24-
/// (An instance of this class is connected to the `flutter/platform_views`
24+
/// (The instance of this class is connected to the `flutter/platform_views`
2525
/// Platform Channel in the [EnginePlatformDispatcher] class.)
2626
///
2727
/// It uses a [PlatformViewManager] to handle the CRUD of the DOM of Platform Views.
2828
/// This `contentManager` is shared across the engine, to perform
2929
/// all operations related to platform views (registration, rendering, etc...),
3030
/// regardless of the rendering backend.
3131
///
32-
/// When the `contents` of a Platform View are created, a [PlatformViewContentHandler]
33-
/// function (passed from the outside) will decide where in the DOM to inject
34-
/// said content.
32+
/// Platform views are injected into the DOM when needed by the correct instance
33+
/// of the active renderer.
3534
///
36-
/// The rendering/compositing of Platform Views can create the other "half" of a
35+
/// The rendering and compositing of Platform Views can create the other "half" of a
3736
/// Platform View: the `slot`, through the [createPlatformViewSlot] method.
3837
///
3938
/// When a Platform View is disposed of, it is removed from the cache (and DOM)
4039
/// directly by the `contentManager`. The canvaskit rendering backend needs to do
4140
/// some extra cleanup of its internal state, but it can do it automatically. See
42-
/// [HtmlViewEmbedder.disposeViews]
41+
/// [HtmlViewEmbedder.disposeViews].
4342
class PlatformViewMessageHandler {
4443
PlatformViewMessageHandler({
45-
required DomElement platformViewsContainer,
46-
PlatformViewManager? contentManager,
47-
}) : _contentManager = contentManager ?? PlatformViewManager.instance,
48-
_platformViewsContainer = platformViewsContainer;
44+
required PlatformViewManager contentManager,
45+
}) : _contentManager = contentManager;
46+
47+
static const String channelName = 'flutter/platform_views';
48+
49+
/// The shared instance of PlatformViewMessageHandler.
50+
///
51+
/// Unless configured differently, this connects to the shared instance of the
52+
/// [PlatformViewManager].
53+
static PlatformViewMessageHandler instance = PlatformViewMessageHandler(
54+
contentManager: PlatformViewManager.instance,
55+
);
4956

5057
final MethodCodec _codec = const StandardMethodCodec();
5158
final PlatformViewManager _contentManager;
52-
final DomElement _platformViewsContainer;
5359

5460
/// Handle a `create` Platform View message.
5561
///
@@ -58,10 +64,12 @@ class PlatformViewMessageHandler {
5864
///
5965
/// (See [PlatformViewManager.registerFactory] for more details.)
6066
///
61-
/// The `contents` are inserted into the [_platformViewsContainer].
62-
///
6367
/// If all goes well, this function will `callback` with an empty success envelope.
6468
/// In case of error, this will `callback` with an error envelope describing the error.
69+
///
70+
/// The `callback` signals when the contents of a given [platformViewId] have
71+
/// been rendered. They're now accessible through `platformViewRegistry.getViewById`
72+
/// from `dart:ui_web`. **(Not the DOM!)**
6573
void _createPlatformView(
6674
_PlatformMessageResponseCallback callback, {
6775
required int platformViewId,
@@ -88,15 +96,12 @@ class PlatformViewMessageHandler {
8896
return;
8997
}
9098

91-
final DomElement content = _contentManager.renderContent(
99+
_contentManager.renderContent(
92100
platformViewType,
93101
platformViewId,
94102
params,
95103
);
96104

97-
// For now, we don't need anything fancier. If needed, this can be converted
98-
// to a PlatformViewStrategy class for each web-renderer backend?
99-
_platformViewsContainer.append(content);
100105
callback(_codec.encodeSuccessEnvelope(null));
101106
}
102107

@@ -126,7 +131,7 @@ class PlatformViewMessageHandler {
126131
/// This is transitional code to support the old platform view channel. As
127132
/// soon as the framework code is updated to send the Flutter View ID, this
128133
/// method can be removed.
129-
void handleLegacyPlatformViewCall(
134+
void handlePlatformViewCall(
130135
String method,
131136
dynamic arguments,
132137
_PlatformMessageResponseCallback callback,
@@ -141,39 +146,11 @@ class PlatformViewMessageHandler {
141146
params: arguments['params'],
142147
);
143148
return;
149+
// TODO(web): Send `arguments` as a Map for `dispose` too!
144150
case 'dispose':
145151
_disposePlatformView(callback, platformViewId: arguments as int);
146152
return;
147153
}
148154
callback(null);
149155
}
150-
151-
/// Handles a PlatformViewCall to the `flutter/platform_views` channel.
152-
///
153-
/// This method handles two possible messages:
154-
/// * `create`: See [_createPlatformView]
155-
/// * `dispose`: See [_disposePlatformView]
156-
void handlePlatformViewCall(
157-
String method,
158-
Map<dynamic, dynamic> arguments,
159-
_PlatformMessageResponseCallback callback,
160-
) {
161-
switch (method) {
162-
case 'create':
163-
_createPlatformView(
164-
callback,
165-
platformViewId: arguments.readInt('platformViewId'),
166-
platformViewType: arguments.readString('platformViewType'),
167-
params: arguments['params'],
168-
);
169-
return;
170-
case 'dispose':
171-
_disposePlatformView(
172-
callback,
173-
platformViewId: arguments.readInt('platformViewId'),
174-
);
175-
return;
176-
}
177-
callback(null);
178-
}
179156
}

lib/web_ui/lib/src/engine/scene_view.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,12 @@ class EngineSceneView {
111111

112112
case PlatformViewSlice():
113113
for (final PlatformView view in slice.views) {
114+
// TODO(harryterkelsen): Inject the FlutterView instance from `renderScene`,
115+
// instead of using `EnginePlatformDispatcher...implicitView` directly,
116+
// or make the FlutterView "register" like in canvaskit.
117+
// Ensure the platform view contents are injected in the DOM.
118+
EnginePlatformDispatcher.instance.implicitView?.dom.injectPlatformView(view.viewId);
119+
114120
// Attempt to reuse a container for the existing view
115121
PlatformViewContainer? container;
116122
for (int j = 0; j < reusableContainers.length; j++) {

lib/web_ui/lib/src/engine/view_embedder/dom_manager.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import 'package:ui/ui.dart' as ui;
66

77
import '../configuration.dart';
88
import '../dom.dart';
9+
import '../platform_views/content_manager.dart';
910
import '../safe_browser_api.dart';
1011
import '../semantics/semantics.dart';
1112
import 'style_manager.dart';
@@ -204,6 +205,33 @@ class DomManager {
204205
sceneHost.append(sceneElement);
205206
}
206207
}
208+
209+
/// Injects a platform view with [platformViewId] into [platformViewsHost].
210+
///
211+
/// If the platform view is already injected, this method does *nothing*.
212+
///
213+
/// The `platformViewsHost` can only be different if `platformViewId` is moving
214+
/// from one [FlutterView] to another. In that case, the browser will move the
215+
/// slot contents from the old `platformViewsHost` to the new one, but that
216+
/// will cause the platformView to reset its state (an iframe will re-render,
217+
/// text selections will be lost, video playback interrupted, etc...)
218+
///
219+
/// Try not to move platform views across views!
220+
void injectPlatformView(int platformViewId) {
221+
// For now, we don't need anything fancier. If needed, this can be converted
222+
// to a PlatformViewStrategy class for each web-renderer backend?
223+
final DomElement? pv = PlatformViewManager.instance.getSlottedContent(platformViewId);
224+
if (pv == null) {
225+
domWindow.console.debug('Failed to inject Platform View Id: $platformViewId. '
226+
'Render seems to be happening before a `flutter/platform_views:create` platform message!');
227+
return;
228+
}
229+
// If pv is already a descendant of platformViewsHost -> noop
230+
if (pv.parent == platformViewsHost) {
231+
return;
232+
}
233+
platformViewsHost.append(pv);
234+
}
207235
}
208236

209237
DomShadowRoot _attachShadowRoot(DomElement element) {

lib/web_ui/lib/src/engine/window.dart

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ import 'mouse/context_menu.dart';
1717
import 'mouse/cursor.dart';
1818
import 'navigation/history.dart';
1919
import 'platform_dispatcher.dart';
20-
import 'platform_views/message_handler.dart';
2120
import 'pointer_binding.dart';
2221
import 'semantics.dart';
2322
import 'services.dart';
@@ -132,9 +131,6 @@ base class EngineFlutterView implements ui.FlutterView {
132131
late final DomManager dom =
133132
DomManager(viewId: viewId, devicePixelRatio: devicePixelRatio);
134133

135-
late final PlatformViewMessageHandler platformViewMessageHandler =
136-
PlatformViewMessageHandler(platformViewsContainer: dom.platformViewsHost);
137-
138134
late final PointerBinding pointerBinding;
139135

140136
// TODO(goderbauer): Provide API to configure constraints. See also TODO in "render".

0 commit comments

Comments
 (0)