Repository navigation
Initial framework support for iOS platform views. - #23412
Conversation
This PR adds the full framework stack (layer->render object->widget, and service) for embedding iOS views with minimal functionality. I allowed myself to throw the entire framework stack in one PR since we're mostly mirroring the structure we already established for embedded Android views, so this PR is a little bigger than usual. I'm happy to break it down to the different pieces of the stack if reviewers prefer. Specifically this PR adds: * A UiKitView widget for embedding a UIView in the widget tree. * A RenderUiKitView which is the render object for showing a UIView. * A PlatformViewLayer which denotes the position of a UIView in the layer tree. * The iOS platform_views system channel client code in services/platform_views.dart * Splits the fake platform views controller to an Android and iOS controllers. TBD in following PRs: * Plumb the layout direction all the way to the platform view (currently there is still no engine support for it). * Integrate with gesture arenas (engine support is still missing as well).
| /// [RenderAndroidView] is responsible for sizing, displaying and passing touch events to an | ||
| /// Android [View](https://developer.android.com/reference/android/view/View). | ||
| /// | ||
| /// {@template platform_render_layout} |
There was a problem hiding this comment.
Be more specific about namespacing. See other usages of @template
|
|
||
| /// A layer that shows an embedded [UIView](https://developer.apple.com/documentation/uikit/uiview) | ||
| /// on iOS. | ||
| class PlatformViewLayer extends Layer { |
There was a problem hiding this comment.
Are we ever going to give a view id to fuchsia and non-iOS to compose? I worry that we'll add actual iOS specific stuff to this class later on and dilute the 'PlatformView' nomenclature since we've been fairly good at calling Android things RenderAndroidView etc.
There was a problem hiding this comment.
I can imagine this getting used for non-iOS use-cases.
Also note that in the engine we abstracted by the platform-agnostic Surface.
| @required this.viewId, | ||
| }): assert(rect != null), assert(viewId != null); | ||
|
|
||
| /// Bounding rectangle of this layer. |
There was a problem hiding this comment.
Mentioned it's in the global coordinate space, hope that clarifies?
| /// | ||
| /// [RenderUiKitView] is responsible for sizing and displaying an iOS | ||
| /// [UIView](https://developer.apple.com/documentation/uikit/uiview). | ||
| /// |
There was a problem hiding this comment.
Add more details about hit testing and how it delegates painting.
There was a problem hiding this comment.
Added a note about how the UIViews are composited (this is still churning so I'm purposely keeping it pretty abstract here).
Hit testing is explained on PlatformViewHitTestBehavior, skimming through proxy_box.dart I see that this is the common practice (e.g we don't normally describe hit testing behavior in the render objects class docs).
Right now touch support is not yet implemented, I'm planning to add some docs about it when adding the support.
| _viewId = viewId; | ||
|
|
||
|
|
||
| /// The unique identifier of the UIView controlled by this controller. |
There was a problem hiding this comment.
Cross reference platform view services here again?
| bool _initialized = false; | ||
|
|
||
| static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet = | ||
| Set<Factory<OneSequenceGestureRecognizer>>(); |
| @override | ||
| Widget build(BuildContext context) { | ||
| if (_controller == null) { | ||
| return const SizedBox.expand(); |
There was a problem hiding this comment.
Should we allow for a custom specifiable placeholder for android and uikitview?
There was a problem hiding this comment.
That's probably useful, though I'd keep it for a future PR, this one is already doing too many things.
| return; | ||
| } | ||
| _initialized = true; | ||
| _layoutDirection = _findLayoutDirection(); |
There was a problem hiding this comment.
I don't think you need this one. Since didChangeDependencies is always called and it always does this anyway.
There was a problem hiding this comment.
We need _layoutDirection set before calling _createNewUiKitView for the fist time...
There was a problem hiding this comment.
Still feel like there's some redundancy. Maybe move lines 339-341 2 lines up?
There was a problem hiding this comment.
Good suggestion, done, it's cleaner now.
| if (widget.onPlatformViewCreated != null) { | ||
| widget.onPlatformViewCreated(_id); | ||
| } | ||
| setState(() {}); |
There was a problem hiding this comment.
final local controller and setState(() { _controller = controller; }); just to be explicit? Or at least add some docs.
| } | ||
| } | ||
|
|
||
| class _UiKitPlatformView extends LeafRenderObjectWidget { |
There was a problem hiding this comment.
I think the convention is calling these XRenderObjectWidget (if you global search for extends LeafRenderObjectWidget)
There was a problem hiding this comment.
Doesn't seem to be a convention...
$ git grep "extends LeafRenderObject" | wc -l
23
$ git grep "RenderObjectWidget extends LeafRenderObject" | wc -l
6
$ git grep "extends LeafRenderObject"
...
lib/src/widgets/basic.dart:class RichText extends LeafRenderObjectWidget {
lib/src/widgets/basic.dart:class RawImage extends LeafRenderObjectWidget {
lib/src/widgets/basic.dart:class WidgetToRenderBoxAdapter extends LeafRenderObjectWidget {
lib/src/widgets/editable_text.dart:class _Editable extends LeafRenderObjectWidget {
lib/src/widgets/framework.dart:class ErrorWidget extends LeafRenderObjectWidget {
lib/src/widgets/performance_overlay.dart:class PerformanceOverlay extends LeafRenderObjectWidget {
...
| /// Sets the layout direction for the Android view. | ||
| Future<void> setLayoutDirection(TextDirection layoutDirection) async { | ||
| if (_disposed) | ||
| throw FlutterError('trying to set a layout direction for a disposed Android View. View id: $id'); |
There was a problem hiding this comment.
i would only check this in debug mode
| /// The unique identifier of the iOS view controlled by this controller. | ||
| final int id; | ||
|
|
||
| bool _disposed = false; |
There was a problem hiding this comment.
i would call this _debugDisposed and only track it in debug mode
| /// | ||
| /// {@macro platform_view_constructor_params} | ||
| UiKitView({ // ignore: prefer_const_constructors_in_immutables | ||
| // TODO(aam): Remove lint ignore above once dartbug.com/34297 is fixed |
There was a problem hiding this comment.
can you make that link a full link (https://... etc)? That way it comes up in searches for links when I do audits. Thanks.
| final TextDirection layoutDirection; | ||
|
|
||
| @override | ||
| State createState() => _UiKitViewState(); |
There was a problem hiding this comment.
State<UiKitView> or _UiKitViewState, not just State.
| viewType: widget.viewType, | ||
| layoutDirection: _layoutDirection, | ||
| ); | ||
| if (widget.onPlatformViewCreated != null) { |
There was a problem hiding this comment.
since this is async, you might get disposed before initUiKitView returns. You should check mounted is true here and return early if it is not.
There was a problem hiding this comment.
Good catch!
Fixed and added a unit test.
| if (widget.onPlatformViewCreated != null) { | ||
| widget.onPlatformViewCreated(_id); | ||
| } | ||
| setState(() {}); |
There was a problem hiding this comment.
always put a comment in empty setState blocks explaining what state changed.
There was a problem hiding this comment.
changed to be non empty setState.
|
|
||
| @override | ||
| void dispose() { | ||
| _controller.dispose(); |
There was a problem hiding this comment.
_controller might be null here
| _layoutDirection = newLayoutDirection; | ||
|
|
||
| if (widget.viewType != oldWidget.viewType) { | ||
| _controller.dispose(); |
There was a problem hiding this comment.
_controller might be null here
| /// apply to it as well. | ||
| /// | ||
| /// The widget fill all available space, the parent of this object must provide bounded layout | ||
| /// {@template platform_view_layout} |
| /// ``` | ||
| /// | ||
| /// The Android view's lifetime is the same as the lifetime of the [State] object for this widget. | ||
| /// {@template platform_view_lifetime} |
|
|
||
| /// The codec used to encode `creationParams` before sending it to the | ||
| /// platform side. It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-). | ||
| /// platform side.It should match the codec passed to the constructor of [PlatformViewFactory](/javadoc/io/flutter/plugin/platform/PlatformViewFactory.html#PlatformViewFactory-io.flutter.plugin.common.MessageCodec-). |
| bool _initialized = false; | ||
|
|
||
| static final Set<Factory<OneSequenceGestureRecognizer>> _emptyRecognizersSet = | ||
| Set<Factory<OneSequenceGestureRecognizer>>(); |
| @override | ||
| Widget build(BuildContext context) { | ||
| if (_controller == null) { | ||
| return const SizedBox.expand(); |
There was a problem hiding this comment.
That's probably useful, though I'd keep it for a future PR, this one is already doing too many things.
| TextDirection layoutDirection, | ||
| ) : assert(id != null), | ||
| assert(layoutDirection != null), | ||
| _layoutDirection = layoutDirection; |
There was a problem hiding this comment.
The layoutDirection will be passed as an argument of the create call (this is not yet implemented in the engine).
| _layoutDirection = layoutDirection; | ||
|
|
||
|
|
||
| /// The unique identifier of the iOS view controlled by this controller. |
| /// Sets the layout direction for the Android view. | ||
| Future<void> setLayoutDirection(TextDirection layoutDirection) async { | ||
| if (_disposed) | ||
| throw FlutterError('trying to set a layout direction for a disposed Android View. View id: $id'); |
| /// Disposes the view. | ||
| /// | ||
| /// The [UiKitViewController] object is unusable after calling this. | ||
| /// The identifier of the platform view cannot be reused after the view is |
| /// | ||
| /// Embedding iOS views is an expensive operation and should be avoided when a Flutter | ||
| /// equivalent is possible. | ||
| /// |
There was a problem hiding this comment.
Added a TODO, there's going to be some churn in the way it works with the incremental progress on the engine side, I'll describe it here once it settled.
| } | ||
|
|
||
| if (didChangeLayoutDirection) { | ||
| _controller.setLayoutDirection(_layoutDirection); |
There was a problem hiding this comment.
_controller might be null here
| if (didChangeLayoutDirection) { | ||
| // The native view will update asynchronously, in the meantime we don't want | ||
| // to block the framework. (so this is intentionally not awaiting). | ||
| _controller.setLayoutDirection(_layoutDirection); |
There was a problem hiding this comment.
_controller might be null here
| RenderUiKitView( | ||
| viewId: viewId, | ||
| hitTestBehavior: hitTestBehavior, | ||
| ); |
There was a problem hiding this comment.
nit: avoid line breaks in => expressions
| return false; | ||
| final FakeIosPlatformView typedOther = other; | ||
| return id == typedOther.id && | ||
| type == typedOther.type; |
| } | ||
| } | ||
|
|
||
| class FakeIosPlatformView { |
There was a problem hiding this comment.
/me peers suspiciously at the "Ios"
There was a problem hiding this comment.
Renamed to FakeUiKitView
|
|
||
| @override | ||
| bool operator ==(dynamic other) { | ||
| if (other is! FakeIosPlatformView) |
There was a problem hiding this comment.
compare runtimeType instead of using is in operator==, otherwise subclasses will have asymmetric == operators
There was a problem hiding this comment.
done (here and in FakeAndroidPlatformView)
|
|
||
| expect(factoryInvocationCount, 1); | ||
| }); | ||
| } |
There was a problem hiding this comment.
consider adding tests that hit the "_controller is null" side of everywhere that you end up testing whether _controller is null (doing so will involve creating a bunch of interesting tests that test edge cases of things changing at various times, disposing early, etc, which is generally valuable)
There was a problem hiding this comment.
added a 'disposeUIView before creation completed' test case
|
Thanks @Hixie ! PTAL |
| return; | ||
| } | ||
| _initialized = true; | ||
| _layoutDirection = _findLayoutDirection(); |
There was a problem hiding this comment.
Still feel like there's some redundancy. Maybe move lines 339-341 2 lines up?
| } | ||
|
|
||
| class FakeAndroidPlatformView { | ||
|
|
| } | ||
|
|
||
| class FakeUiKitView { | ||
|
|
| layoutDirection: TextDirection.ltr, | ||
| ); | ||
| expect( | ||
| () => |
There was a problem hiding this comment.
reformatted with dartfmt. (and it's weird)
| unorderedEquals(<FakeUiKitView>[ | ||
| FakeUiKitView(0, 'webview'), | ||
| FakeUiKitView(1, 'webview'), | ||
| ])); |
There was a problem hiding this comment.
trailing commas might get the formatter to return something cleaner
amirh
left a comment
There was a problem hiding this comment.
Thanks, I'll wait for some relevant engine stuff to land before landing this.
| return; | ||
| } | ||
| _initialized = true; | ||
| _layoutDirection = _findLayoutDirection(); |
There was a problem hiding this comment.
Good suggestion, done, it's cleaner now.
| } | ||
|
|
||
| class FakeAndroidPlatformView { | ||
|
|
| } | ||
|
|
||
| class FakeUiKitView { | ||
|
|
| layoutDirection: TextDirection.ltr, | ||
| ); | ||
| expect( | ||
| () => |
There was a problem hiding this comment.
reformatted with dartfmt. (and it's weird)
| unorderedEquals(<FakeUiKitView>[ | ||
| FakeUiKitView(0, 'webview'), | ||
| FakeUiKitView(1, 'webview'), | ||
| ])); |
This PR adds the full framework stack (layer->render object->widget, and service) for embedding iOS views with minimal functionality. I allowed myself to throw the entire framework stack in one PR since we're mostly mirroring the structure we already established for embedded Android views, so this PR is a little bigger than usual. I'm happy to break it down to the different pieces of the stack if reviewers prefer. Specifically this PR adds: * A UiKitView widget for embedding a UIView in the widget tree. * A RenderUiKitView which is the render object for showing a UIView. * A PlatformViewLayer which denotes the position of a UIView in the layer tree. * The iOS platform_views system channel client code in services/platform_views.dart * Splits the fake platform views controller to an Android and iOS controllers. TBD in following PRs: * Plumb the layout direction all the way to the platform view (currently there is still no engine support for it). * Integrate with gesture arenas (engine support is still missing as well).
)" (flutter#23779) This reverts commit 67ffe1c.
…23781) Re-landing flutter#23412 with a fix to the PlatformLayer's addToScene signature. flutter#23412 was broken by the change to Layer done in flutter#23434. It seemed green as the presubmits were done before flutter#23434 was landed, and when flutter#23412 landed it broke the build. Reverts flutter#23779
This PR adds the full framework stack (layer->render object->widget, and
service) for embedding iOS views with minimal functionality (see below for what's still missing).
I allowed myself to throw the entire framework stack in one PR since we're mostly
mirroring the structure we already established for embedded Android views, so this PR
is a little bigger than usual. I'm happy to break it down to the
different pieces of the stack if reviewers prefer.
Specifically this PR adds:
UIView.
layer tree.
controllers.
TBD in following PRs:
there is still no engine support for it).
well).
#19030