Skip to content

Commit

Permalink
Reland: Fix Android platform view creation flow (#109405)
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartmorgan committed Aug 12, 2022
1 parent b79c216 commit ab23233
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 40 deletions.
14 changes: 3 additions & 11 deletions packages/flutter/lib/src/rendering/platform_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
74 changes: 52 additions & 22 deletions packages/flutter/lib/src/services/platform_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -763,16 +763,35 @@ abstract class AndroidViewController extends PlatformViewController {
/// Sends the message to dispose the platform view.
Future<void> _sendDisposeMessage();

/// True if [_sendCreateMessage] can only be called with a non-null size.
bool get _createRequiresSize;

/// Sends the message to create the platform view with an initial [size].
Future<void> _sendCreateMessage({Size? size});
///
/// If [_createRequiresSize] is true, `size` is non-nullable, and the call
/// should instead be deferred until the size is available.
Future<void> _sendCreateMessage({required covariant Size? size});

/// Sends the message to resize the platform view to [size].
Future<Size> _sendResizeMessage(Size size);

@override
bool get awaitingCreation => _state == _AndroidViewState.waitingForSize;

@override
Future<void> create({Size? size}) async {
assert(_state != _AndroidViewState.disposed, 'trying to create a disposed Android view');
assert(_state == _AndroidViewState.waitingForSize, 'Android view is already sized. View id: $viewId');

await _sendCreateMessage(size: size);
if (_createRequiresSize && size == null) {
// Wait for a setSize call.
return;
}

_state = _AndroidViewState.creating;
await _sendCreateMessage(size: size);
_state = _AndroidViewState.created;

for (final PlatformViewCreatedCallback callback in _platformViewCreatedCallbacks) {
callback(viewId);
}
Expand All @@ -792,7 +811,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<Size> setSize(Size size);
Future<Size> 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.
///
Expand Down Expand Up @@ -972,7 +1001,10 @@ class ExpensiveAndroidViewController extends AndroidViewController {
}) : super._();

@override
Future<void> _sendCreateMessage({Size? size}) {
bool get _createRequiresSize => false;

@override
Future<void> _sendCreateMessage({required Size? size}) async {
final Map<String, dynamic> args = <String, dynamic>{
'id': viewId,
'viewType': _viewType,
Expand All @@ -988,7 +1020,7 @@ class ExpensiveAndroidViewController extends AndroidViewController {
paramsByteData.lengthInBytes,
);
}
return SystemChannels.platform_views.invokeMethod<void>('create', args);
await SystemChannels.platform_views.invokeMethod<void>('create', args);
}

@override
Expand All @@ -1005,7 +1037,7 @@ class ExpensiveAndroidViewController extends AndroidViewController {
}

@override
Future<Size> setSize(Size size) {
Future<Size> _sendResizeMessage(Size size) {
throw UnimplementedError('Not supported for $SurfaceAndroidViewController.');
}

Expand Down Expand Up @@ -1044,8 +1076,7 @@ class TextureAndroidViewController extends AndroidViewController {
Offset _off = Offset.zero;

@override
Future<Size> setSize(Size size) async {
assert(_state != _AndroidViewState.disposed, 'Android view is disposed. View id: $viewId');
Future<Size> _sendResizeMessage(Size size) async {
assert(_state != _AndroidViewState.waitingForSize, 'Android view must have an initial size. View id: $viewId');
assert(size != null);
assert(!size.isEmpty);
Expand All @@ -1064,16 +1095,6 @@ class TextureAndroidViewController extends AndroidViewController {
return Size(meta!['width']! as double, meta['height']! as double);
}

@override
Future<void> 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<void> setOffset(Offset off) async {
if (off == _off) {
Expand All @@ -1100,11 +1121,11 @@ class TextureAndroidViewController extends AndroidViewController {
}

@override
Future<void> _sendCreateMessage({Size? size}) async {
if (size == null) {
return;
}
bool get _createRequiresSize => true;

@override
// Size is non-nullable due to _createRequiresSize returning true.
Future<void> _sendCreateMessage({required Size size}) async {
assert(!size.isEmpty, 'trying to create $TextureAndroidViewController without setting a valid size.');

final Map<String, dynamic> args = <String, dynamic>{
Expand Down Expand Up @@ -1219,6 +1240,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<void> dispatchPointerEvent(PointerEvent event);

Expand Down
14 changes: 10 additions & 4 deletions packages/flutter/lib/src/widgets/platform_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -871,14 +871,20 @@ class _PlatformViewLinkState extends State<PlatformViewLink> {

@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,
Expand Down
15 changes: 13 additions & 2 deletions packages/flutter/test/services/fake_platform_views.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<PointerEvent> dispatchedPointerEvents = <PointerEvent>[];
Expand Down Expand Up @@ -92,6 +97,9 @@ class FakeAndroidViewController implements AndroidViewController {
@override
int get textureId => 0;

@override
bool get awaitingCreation => !_createCalledSuccessfully;

@override
bool get isCreated => created;

Expand All @@ -114,7 +122,10 @@ class FakeAndroidViewController implements AndroidViewController {
}

@override
Future<void> create({Size? size}) async {}
Future<void> create({Size? size}) async {
assert(!_createCalledSuccessfully);
_createCalledSuccessfully = size != null || !requiresSize;
}

@override
List<PlatformViewCreatedCallback> get createdCallbacks => <PlatformViewCreatedCallback>[];
Expand Down
106 changes: 105 additions & 1 deletion packages/flutter/test/widgets/platform_view_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1026,6 +1026,7 @@ void main() {

containerFocusNode.requestFocus();

viewsController.createCompleter!.complete();
await tester.pump();

expect(containerFocusNode.hasFocus, isTrue);
Expand Down Expand Up @@ -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 <Factory<OneSequenceGestureRecognizer>>{},
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
);

await tester.pumpWidget(platformViewLink);

expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']),
);

onPlatformViewCreatedCallBack(createdPlatformViewId);

await tester.pump();

expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['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 <Factory<OneSequenceGestureRecognizer>>{},
controller: controller,
hitTestBehavior: PlatformViewHitTestBehavior.opaque,
);
},
);

await tester.pumpWidget(platformViewLink);

expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['PlatformViewLink', '_PlatformViewPlaceHolder']),
);

onPlatformViewCreatedCallBack(createdPlatformViewId);

await tester.pump();

expect(
tester.allWidgets.map((Widget widget) => widget.runtimeType.toString()).toList(),
equals(<String>['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(
Expand Down

0 comments on commit ab23233

Please sign in to comment.