diff --git a/packages/flutter/lib/src/rendering/platform_view.dart b/packages/flutter/lib/src/rendering/platform_view.dart index 5ab23e1c86..1dcabf4fad 100644 --- a/packages/flutter/lib/src/rendering/platform_view.dart +++ b/packages/flutter/lib/src/rendering/platform_view.dart @@ -179,17 +179,9 @@ class RenderAndroidView extends PlatformViewRenderBox { Size targetSize; do { targetSize = size; - if (_viewController.isCreated) { - _currentTextureSize = await _viewController.setSize(targetSize); - if (_isDisposed) { - return; - } - } else { - await _viewController.create(size: targetSize); - if (_isDisposed) { - return; - } - _currentTextureSize = targetSize; + _currentTextureSize = await _viewController.setSize(targetSize); + if (_isDisposed) { + return; } // We've resized the platform view to targetSize, but it is possible that // while we were resizing the render object's size was changed again. diff --git a/packages/flutter/lib/src/services/platform_views.dart b/packages/flutter/lib/src/services/platform_views.dart index 25af4c9fad..25e695d599 100644 --- a/packages/flutter/lib/src/services/platform_views.dart +++ b/packages/flutter/lib/src/services/platform_views.dart @@ -764,17 +764,33 @@ abstract class AndroidViewController extends PlatformViewController { Future _sendDisposeMessage(); /// Sends the message to create the platform view with an initial [size]. - Future _sendCreateMessage({Size? size}); + /// + /// Returns true if the view was actually created. In some cases (e.g., + /// trying to create a texture-based view with a null size) creation will + /// fail and need to be re-attempted later. + Future _sendCreateMessage({Size? size}); + + /// Sends the message to resize the platform view to [size]. + Future _sendResizeMessage(Size size); + + @override + bool get awaitingCreation => _state == _AndroidViewState.waitingForSize; @override Future create({Size? size}) async { assert(_state != _AndroidViewState.disposed, 'trying to create a disposed Android view'); - await _sendCreateMessage(size: size); + assert(_state == _AndroidViewState.waitingForSize, 'Android view is already sized. View id: $viewId'); + _state = _AndroidViewState.creating; + final bool created = await _sendCreateMessage(size: size); - _state = _AndroidViewState.created; - for (final PlatformViewCreatedCallback callback in _platformViewCreatedCallbacks) { - callback(viewId); + if (created) { + _state = _AndroidViewState.created; + for (final PlatformViewCreatedCallback callback in _platformViewCreatedCallbacks) { + callback(viewId); + } + } else { + _state = _AndroidViewState.waitingForSize; } } @@ -792,7 +808,17 @@ abstract class AndroidViewController extends PlatformViewController { /// /// As a result, consumers are expected to clip the texture using [size], while using /// the return value to size the texture. - Future setSize(Size size); + Future setSize(Size size) async { + assert(_state != _AndroidViewState.disposed, 'Android view is disposed. View id: $viewId'); + if (_state == _AndroidViewState.waitingForSize) { + // Either `create` hasn't been called, or it couldn't run due to missing + // size information, so create the view now. + await create(size: size); + return size; + } else { + return _sendResizeMessage(size); + } + } /// Sets the offset of the platform view. /// @@ -972,7 +998,7 @@ class ExpensiveAndroidViewController extends AndroidViewController { }) : super._(); @override - Future _sendCreateMessage({Size? size}) { + Future _sendCreateMessage({Size? size}) async { final Map args = { 'id': viewId, 'viewType': _viewType, @@ -988,7 +1014,8 @@ class ExpensiveAndroidViewController extends AndroidViewController { paramsByteData.lengthInBytes, ); } - return SystemChannels.platform_views.invokeMethod('create', args); + await SystemChannels.platform_views.invokeMethod('create', args); + return true; } @override @@ -1005,7 +1032,7 @@ class ExpensiveAndroidViewController extends AndroidViewController { } @override - Future setSize(Size size) { + Future _sendResizeMessage(Size size) { throw UnimplementedError('Not supported for $SurfaceAndroidViewController.'); } @@ -1044,8 +1071,7 @@ class TextureAndroidViewController extends AndroidViewController { Offset _off = Offset.zero; @override - Future setSize(Size size) async { - assert(_state != _AndroidViewState.disposed, 'Android view is disposed. View id: $viewId'); + Future _sendResizeMessage(Size size) async { assert(_state != _AndroidViewState.waitingForSize, 'Android view must have an initial size. View id: $viewId'); assert(size != null); assert(!size.isEmpty); @@ -1064,16 +1090,6 @@ class TextureAndroidViewController extends AndroidViewController { return Size(meta!['width']! as double, meta['height']! as double); } - @override - Future create({Size? size}) async { - if (size == null) { - return; - } - assert(_state == _AndroidViewState.waitingForSize, 'Android view is already sized. View id: $viewId'); - assert(!size.isEmpty); - return super.create(size: size); - } - @override Future setOffset(Offset off) async { if (off == _off) { @@ -1100,9 +1116,9 @@ class TextureAndroidViewController extends AndroidViewController { } @override - Future _sendCreateMessage({Size? size}) async { + Future _sendCreateMessage({Size? size}) async { if (size == null) { - return; + return false; } assert(!size.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.'); @@ -1123,6 +1139,7 @@ class TextureAndroidViewController extends AndroidViewController { ); } _textureId = await SystemChannels.platform_views.invokeMethod('create', args); + return true; } @override @@ -1219,6 +1236,15 @@ abstract class PlatformViewController { /// * [PlatformViewsRegistry], which is a helper for managing platform view IDs. int get viewId; + /// True if [create] has not been successfully called the platform view. + /// + /// This can indicate either that [create] was never called, or that [create] + /// was deferred for implementation-specific reasons. + /// + /// A `false` return value does not necessarily indicate that the [Future] + /// returned by [create] has completed, only that creation has been started. + bool get awaitingCreation => false; + /// Dispatches the `event` to the platform view. Future dispatchPointerEvent(PointerEvent event); diff --git a/packages/flutter/lib/src/widgets/platform_view.dart b/packages/flutter/lib/src/widgets/platform_view.dart index 2dac509fd9..c2a1f87982 100644 --- a/packages/flutter/lib/src/widgets/platform_view.dart +++ b/packages/flutter/lib/src/widgets/platform_view.dart @@ -871,14 +871,20 @@ class _PlatformViewLinkState extends State { @override Widget build(BuildContext context) { - if (_controller == null) { + final PlatformViewController? controller = _controller; + if (controller == null) { return const SizedBox.expand(); } if (!_platformViewCreated) { - // Depending on the platform, the initial size can be used to size the platform view. - return _PlatformViewPlaceHolder(onLayout: (Size size) => _controller!.create(size: size)); + // Depending on the implementation, the initial size can be used to size + // the platform view. + return _PlatformViewPlaceHolder(onLayout: (Size size) { + if (controller.awaitingCreation) { + controller.create(size: size); + } + }); } - _surface ??= widget._surfaceFactory(context, _controller!); + _surface ??= widget._surfaceFactory(context, controller); return Focus( focusNode: _focusNode, onFocusChange: _handleFrameworkFocusChanged, diff --git a/packages/flutter/test/services/fake_platform_views.dart b/packages/flutter/test/services/fake_platform_views.dart index d5ebe534c1..5210478802 100644 --- a/packages/flutter/test/services/fake_platform_views.dart +++ b/packages/flutter/test/services/fake_platform_views.dart @@ -45,11 +45,16 @@ class FakePlatformViewController extends PlatformViewController { } class FakeAndroidViewController implements AndroidViewController { - FakeAndroidViewController(this.viewId); + FakeAndroidViewController(this.viewId, {this.requiresSize = false}); bool disposed = false; bool focusCleared = false; bool created = false; + // If true, [create] won't be considered to have been called successfully + // unless it includes a size. + bool requiresSize; + + bool _createCalledSuccessfully = false; /// Events that are dispatched. List dispatchedPointerEvents = []; @@ -92,6 +97,9 @@ class FakeAndroidViewController implements AndroidViewController { @override int get textureId => 0; + @override + bool get awaitingCreation => !_createCalledSuccessfully; + @override bool get isCreated => created; @@ -114,7 +122,10 @@ class FakeAndroidViewController implements AndroidViewController { } @override - Future create({Size? size}) async {} + Future create({Size? size}) async { + assert(!_createCalledSuccessfully); + _createCalledSuccessfully = size != null || !requiresSize; + } @override List get createdCallbacks => []; diff --git a/packages/flutter/test/widgets/platform_view_test.dart b/packages/flutter/test/widgets/platform_view_test.dart index 227b00c94a..7c15a7bef0 100644 --- a/packages/flutter/test/widgets/platform_view_test.dart +++ b/packages/flutter/test/widgets/platform_view_test.dart @@ -1026,6 +1026,7 @@ void main() { containerFocusNode.requestFocus(); + viewsController.createCompleter!.complete(); await tester.pump(); expect(containerFocusNode.hasFocus, isTrue); @@ -2423,7 +2424,110 @@ void main() { onCreatePlatformView: (PlatformViewCreationParams params) { onPlatformViewCreatedCallBack = params.onPlatformViewCreated; createdPlatformViewId = params.id; - return FakePlatformViewController(params.id); + return FakePlatformViewController(params.id)..create(); + }, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return PlatformViewSurface( + gestureRecognizers: const >{}, + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + ); + + await tester.pumpWidget(platformViewLink); + + expect( + tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), + equals(['PlatformViewLink', '_PlatformViewPlaceHolder']), + ); + + onPlatformViewCreatedCallBack(createdPlatformViewId); + + await tester.pump(); + + expect( + tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), + equals(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), + ); + + expect(createdPlatformViewId, currentViewId + 1); + }, + ); + + testWidgets( + 'PlatformViewLink calls create when needed for Android texture display modes', + (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + late int createdPlatformViewId; + + late PlatformViewCreatedCallback onPlatformViewCreatedCallBack; + late PlatformViewController controller; + + final PlatformViewLink platformViewLink = PlatformViewLink( + viewType: 'webview', + onCreatePlatformView: (PlatformViewCreationParams params) { + onPlatformViewCreatedCallBack = params.onPlatformViewCreated; + createdPlatformViewId = params.id; + controller = FakeAndroidViewController(params.id, requiresSize: true); + controller.create(); + // This test should be simulating one of the texture-based display + // modes, where `create` is a no-op when not provided a size, and + // creation is triggered via a later call to setSize, or to `create` + // with a size. + expect(controller.awaitingCreation, true); + return controller; + }, + surfaceFactory: (BuildContext context, PlatformViewController controller) { + return PlatformViewSurface( + gestureRecognizers: const >{}, + controller: controller, + hitTestBehavior: PlatformViewHitTestBehavior.opaque, + ); + }, + ); + + await tester.pumpWidget(platformViewLink); + + expect( + tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), + equals(['PlatformViewLink', '_PlatformViewPlaceHolder']), + ); + + onPlatformViewCreatedCallBack(createdPlatformViewId); + + await tester.pump(); + + expect( + tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(), + equals(['PlatformViewLink', 'Focus', '_FocusMarker', 'Semantics', 'PlatformViewSurface']), + ); + + expect(createdPlatformViewId, currentViewId + 1); + expect(controller.awaitingCreation, false); + }, + ); + + testWidgets( + 'PlatformViewLink does not double-call create for Android Hybrid Composition', + (WidgetTester tester) async { + final int currentViewId = platformViewsRegistry.getNextPlatformViewId(); + late int createdPlatformViewId; + + late PlatformViewCreatedCallback onPlatformViewCreatedCallBack; + late PlatformViewController controller; + + final PlatformViewLink platformViewLink = PlatformViewLink( + viewType: 'webview', + onCreatePlatformView: (PlatformViewCreationParams params) { + onPlatformViewCreatedCallBack = params.onPlatformViewCreated; + createdPlatformViewId = params.id; + controller = FakeAndroidViewController(params.id); + controller.create(); + // This test should be simulating Hybrid Composition mode, where + // `create` takes effect immidately. + expect(controller.awaitingCreation, false); + return controller; }, surfaceFactory: (BuildContext context, PlatformViewController controller) { return PlatformViewSurface(