Integrating PlatformView into Flutter (iOS/macOS)
- Pavel Kalinin

- 2 окт.
- 10 мин. чтения
Flutter has gained popularity for its ability to create beautiful, fast, and cross-platform applications from a single codebase. However, any developer tackling complex tasks will eventually ask, "What if I need a feature that is tightly integrated with the native platform, such as advanced camera control, specific maps, or unique OS UI elements?"
Fortunately, Flutter provides two powerful mechanisms to solve this problem: FlutterPlatformView and FlutterTexture. These tools allow you to embed native components directly into the Flutter widget tree, but they do so in completely different ways.
In this article, we'll take a deep dive into both approaches. We'll explore their strengths and weaknesses, and we'll implement an iOS/macOS camera plugin using both technologies. This will allow you to see the difference in practice and choose the right tool for your specific needs.

Firstly, I recommend reading the article of PlatformView integration for Android, as it covers general concepts that will also be relevant in this article.
This article provides a detailed guide on integrating PlatformView into Flutter on iOS by embedding an iOS/macOS view component. Using the camera as an example, we’ll compare two approaches to help you choose the one that best fits your project.
Details of PlatformView Implementation on iOS/macOS: Hybrid Composition and Texture Layer
On iOS/macOS, unlike Android where we discussed three integration options (Virtual Display, Hybrid Composition, and Texture Layer), there are two primary approaches for integrating native UI: Hybrid Composition and Texture Layer. The choice between them depends on one key question: do you need to interact with the native UI, or do you simply need to display pixels?
Hybrid Composition (FlutterPlatformView)
Hybrid Composition is essentially a "window" into the native world, allowing you to embed a fully interactive UIView directly into the Flutter widget tree. This approach is similar to Hybrid Composition on Android, as both allow you to integrate full-fledged native components while preserving their interactivity.
How It Works: Flutter creates a "hole" in its render, through which the native component is displayed on the screen. Flutter's compositing system then "stitches" the two rendering worlds together, ensuring correct layering
Pros:
Full Interactivity: All gestures, taps, and input events are passed directly to the native component. This is ideal for maps, web views, or complex controls
Easy for Existing Components: If you already have a complex native UI component, it's easy to integrate without rewriting it
Cons:
Performance: This is a "heavy" operation. "Stitching" two different rendering worlds can cause performance drops, especially in animations or long lists
"Airspace Issues": In older versions of Flutter, the native widget always rendered on top of all Flutter UI, causing problems with pop-ups and animations. While the situation is much better now, it still requires careful setup
When to Use: When you need full interactivity with a native component, such as for maps (MapView), web views (WKWebView), or complex ad banners
Texture Layer (FlutterTexture)
Texture Layer is a high-performance, but one-way, channel for transferring pixels. The native side generates an image, and Flutter simply displays it as a texture. This approach is similar to Texture Layer on Android, as both focus on high-performance data transfer via textures, making them ideal for streaming content.
How It Works: The native code gets access to the GPU and provides a texture ID to Flutter. The Flutter engine uses this ID to access the data and render the texture on the screen just like any other widget
Pros:
High Performance: Data exchange happens at the GPU level, making this method perfect for video streaming, camera previews, or games
Full Integration with Flutter: A texture is a regular widget. You can animate it, apply transformations to it, use shaders, and place it beneath other Flutter widgets without any problems
Cons:
No Interactivity: It's just a picture. You can't tap on a button that is part of the native frame, as all interactive elements must be implemented on top of the texture using Flutter widgets
When to Use: When you need to efficiently display a stream of pixels. It's ideal for camera previews, video players, AR, or other visualizations where a high frame rate and low latency are critical
In this article, we'll implement both approaches to showcase a balanced and modern solution. We'll use Hybrid Composition as a general-purpose method for its full native interactivity, and the Texture Layer approach as an optimized solution specifically designed for high-performance use cases like camera feeds.
Creating the Application
To demonstrate this, we'll build a simple camera plugin. We'll use Pigeon to define an API that is split into two layers: a factory API (CameraApi) for creating and destroying instances, and an instance API (CameraInstanceApi) for controlling a specific camera.
1. Project Structure and Setup
Create a new Flutter plugin and add pigeon to your dev_dependencies in pubspec.yaml:
dev_dependencies:
pigeon: ^26.0.0Create your Pigeon API using the camera_api.dart file you provided. This file defines three key APIs:
CameraApi: The factory API for initializing and creating/disposing of camera instances. It's responsible for creating a TextureView or PlatformView.
CameraInstanceApi: The instance API for controlling a specific camera. This will be called with a suffix matching the cameraId, allowing the Flutter side to communicate with a particular instance.
PlatformViewCallbackApi: An API that enables the native side to call methods in Flutter (for example, to notify when video recording has finished).
Generate the code using your camera_api.dart file:
dart run pigeon --input pigeons/camera_api.dart2. iOS/macOS Logic (Swift)
To avoid code duplication, we will create a class hierarchy that mirrors the modular structure of our Pigeon API.
2.1. CameraManager
The CameraManager is a class that implements the CameraInstanceApi protocol and several AVCapture delegates, encapsulating all the core camera logic, such as setup, photo capture, video recording, and zoom. It manages the camera operations independently of the specific UI components.
configure() Method: This private method is responsible for setting up the capture session, configuring inputs and outputs, and preparing the camera for use. This core logic ensures the camera is ready to perform tasks like capturing photos or recording video.
captureOutput() and photoOutput() Methods: These methods are part of the AVCaptureVideoDataOutputSampleBufferDelegate and AVCapturePhotoCaptureDelegate protocols, respectively. They handle the processing of camera data streams and captured photos, acting as callbacks that receive the output from the camera's capture session.
dispose() Method: A common method used to properly close the camera session and release all associated resources, ensuring the camera is no longer in use.
2.2. TextureCameraManager and PlatformViewCameraManager
The TextureCameraManager is a concrete class that extends CameraManager, specializing in providing camera stream output to a texture. This class leverages the core logic of its parent while implementing its own specific methods for texture-based display.
captureOutput() Method: This method, overridden from the parent class, is the core of TextureCameraManager. It receives each new video frame as a CMSampleBuffer, extracts the CVPixelBuffer, and sets it as the latest pixel buffer for the texture. This ensures that the texture is constantly updated with the live camera feed.
dispose() Method: This method extends the parent's disposal logic. In addition to releasing camera resources, it specifically unregisters the texture from the registry and disposes of the texture itself, guaranteeing that all resources related to the texture-based display are properly cleaned up.
The DefaultFlutterTexture is a class that implements the FlutterTexture protocol. Its primary responsibility is to serve as a bridge between the native camera feed (specifically, CVPixelBuffers) and the Flutter rendering engine. It manages the lifecycle of the pixel buffer data, ensuring that the latest video frame is always available for Flutter to display.
setLatestPixelBuffer() Method: This method receives a CVPixelBuffer containing a new video frame from the camera's output. It updates the internal latestPixelBuffer property, making the new frame available for rendering. The use of a dispatch queue ensures that this process is thread-safe, preventing data corruption when multiple threads are involved.
copyPixelBuffer() Method: This method is the key component for integration with Flutter. It's the callback Flutter uses to request the latest video frame from the native side. The method returns the latestPixelBuffer, and then immediately sets the internal reference to nil to prevent displaying the same frame twice. This ensures a smooth and efficient stream of unique video frames to the UI.
dispose() Method: This method handles the cleanup. By setting latestPixelBuffer to nil, it releases the reference to the pixel buffer, freeing up the memory and ensuring no resources are leaked when the camera texture is no longer needed.
The PlatformViewCameraManager is a specialized concrete class that extends CameraManager. Its key purpose is to provide a live camera preview for display within a native view component, a PlatformView in Flutter's context. It achieves this by managing an AVCaptureVideoPreviewLayer, which directly renders the camera feed.
previewLayer: This lazy-initialized property is an instance of AVCaptureVideoPreviewLayer. It is directly connected to the camera's capture session, which means it receives and displays the video stream in real-time.
getPreviewLayer() Method: This public method provides external classes, such as the PlatformView, with access to the previewLayer. This allows the native view to add the layer to its hierarchy, making the camera preview visible to the user. This design pattern effectively separates the camera's underlying logic from the UI component responsible for its display.
For clarity, I created a class interaction diagram that illustrates the previously described relationships.

2.3. PlatformView (CameraPreview and CameraPreviewFactory)
The CameraPreviewFactory acts as a factory for native FlutterPlatformViews/NSView. When the Flutter UiKitView/AppKitView widget requests to create a view, the factory uses the passed cameraId from creationParams to retrieve the corresponding CameraManager from the CameraDarwinPlugin. It then creates and returns an instance of CameraPreview, passing the CameraManager to it, which in this case is the PlatformViewCameraManager.
The CameraPreview class serves as the platform-specific view that bridges Flutter's UI with the native camera preview. Its implementation is tailored for both iOS and macOS using conditional compilation.
On iOS, CameraPreview is an NSObject that implements the FlutterPlatformView protocol. Its primary role is to create and manage a custom CameraPreviewView, which is a UIView subclass. This separation addresses layout timing issues where the initial frame from Flutter might have a zero size. The inner CameraPreviewView wraps the AVCaptureVideoPreviewLayer and overrides the layoutSubviews method to ensure the layer's frame is correctly resized to match the view's bounds once Flutter provides the final layout.
On macOS, the implementation is more direct. The CameraPreview class itself is an NSView subclass and directly manages the AVCaptureVideoPreviewLayer. To enable this, the view is configured with wantsLayer = true, which allows the AVCaptureVideoPreviewLayer to be added as a sublayer. Instead of layoutSubviews, the layout method is overridden to resize the preview layer to the view's bounds, accomplishing the same goal of ensuring the camera feed correctly fills the space allocated by Flutter.

The CameraDarwinPlugin is the central class of the plugin, acting as a high-level coordinator that manages the lifecycle of camera instances and handles communication between Flutter and the native side. It implements the necessary Flutter interfaces like FlutterPlugin and the Pigeon API CameraApi.
Lifecycle Management
The class manages its own lifecycle and that of the camera instances it creates. It implements the necessary FlutterPlugin register() and detachFromEngine() methods for connecting and disconnecting from the Flutter engine.
Camera Instance Factory
The core logic for creating new cameras resides in the create() method. This method acts as a factory, intelligently deciding whether to create a TextureCameraManager for texture-based rendering or a PlatformViewCameraManager for a native UIView, based on the viewType parameter received from Flutter. It then assigns a unique cameraId to the new instance and stores a reference to it in the cameraManagers dictionary for later access.
Communication and State Management
For each newly created camera, the plugin sets up a dedicated communication channel using Pigeon's CameraInstanceApi with a unique suffix based on the cameraId. This architecture allows Flutter to send commands to a specific camera instance. The cameraManagers dictionary is central to this design, providing a single source of truth for tracking and managing all active cameras by their unique IDs. The dispose() method uses this dictionary to find and release resources for a specific camera instance when it's no longer needed.
3. Flutter Logic
3.1 PlatformViewPlatform – The Cross-Platform API Abstraction
The details of PlatformViewPlatform are covered in the Android PlatformView integration article. If you have already read that article, you can continue here without missing anything important.
If you haven’t, it’s recommended to at least review the description of this class before proceeding, so the concepts discussed in this section will make more sense. Once familiar, you can return here to continue
3.2 DarwinCamera – The iOS/macOS Implementation
DarwinCamera is the concrete implementation of CameraPlatform for the iOS/macOS platform. It connects the high-level Dart API to the Pigeon-generated native code.
Initialization and State Management: This class manages the state of each camera instance using internal dictionary like cameras and cameraViewStates. The createWithOptions method saves the camera's view type (textureView or platformView) to determine which widget to build later.
Command Delegation: Methods like switchCamera and takePicture simply delegate their calls to the appropriate CameraInstanceApi instance, which in turn communicates with the native side.
Event Handling: It uses HostCameraMessageHandler to listen for native callbacks via Pigeon and funnel them into a Stream, which allows the Flutter side to handle events like a video recording finishing.
Helper Widgets (TextureCamera, PlatformViewCamera)
These simple StatelessWidget classes serve for the final rendering of the camera stream. They abstract direct work with the low-level Texture and UiKitView/AppKitView widgets from the developer, providing a clean and encapsulated interface.
TextureCamera: This widget wraps the built-in Texture widget. It accepts the textureId (which is the same as the cameraId) and uses it to display the data stream from the native surface.
PlatformViewCamera: This widget wraps the UiKitView/AppKitView. It passes the registered viewType and creationParams (cameraId) to the native side, which allows the native factory to create and connect the corresponding FlutterPlatformView.
3.3. CameraController and CameraPreview – The Developer's API
The CameraController is the main class a developer interacts with. It extends ValueNotifier<CameraValue>, which allows widgets to easily react to changes in the camera's state, such as starting a video recording or completing a photo capture.
Initialization: The initialize() method is asynchronous and key to the controller's lifecycle. It calls createWithOptions() method, passing the chosen viewType, and receives a unique cameraId from the native side.
State Management: CameraController uses an internal CameraValue class to track states like isInitialized, isRecordingVideo, and isTakingPicture.
Commands: Methods like startRecording(), stopRecording(), takePicture(), and etc. simply delegate their calls to the corresponding _cameraPlatform methods.
Events: To handle asynchronous events from the native side, startRecording() subscribes to the onRecordingFinished() stream.
Resource Disposal: The dispose() method ensures that all resources on the native side are properly released.
The CameraPreview is a StatefulWidget that displays the camera feed on the screen. It listens to the CameraController to get the cameraId. Once a valid cameraId is available, it uses buildViewWithOptions() to render the correct widget (Texture or AndroidViewSurface). This architecture ensures a complete separation between the control logic (CameraController) and the display logic (CameraPreview), making the code robust and easy to test.
Conclusion
Native code integration is not just a workaround in Flutter – it's a way to extend the framework's core capabilities. FlutterPlatformView and FlutterTexture represent two distinct strategies for blending native power with Flutter's flexibility. FlutterPlatformView is ideal for embedding interactive components directly into your widget tree, while FlutterTexture excels in scenarios where performance and smooth media rendering are critical.
The real power of these tools comes from a thoughtful architecture. By using abstractions like CameraPlatform and communication tools like Pigeon, you can design adaptable and future-proof systems. This approach allows you to switch between integration methods with minimal changes, keeping your Flutter codebase clean and maintainable. Full project code is available below 📂.
Let’s Build the Future Together
At Igniscor, we go beyond standard Flutter development – we deliver seamless integrations that bridge native power with cross-platform flexibility. Whether it’s embedding native views or building smooth, high-performance interfaces, we transform complex ideas into elegant solutions.
Got a project in mind? Let’s make it happen – contact us today! 🚀








Комментарии