How to Embed Native UI in Flutter The Simple Way

How to Embed Native UI in Flutter The Simple Way

Your Flutter app is great but you need a specific feature that only a native SDK provides. Maybe it’s a specialized map view or a proprietary video player. The SDK gives you a native UI component a UIView on iOS or a View on Android. What do you do?

The first instinct is often to try to reimplement the entire component in Dart. This is almost always a mistake. It’s a huge amount of work and you’ll spend forever chasing bugs and parity with the native version. The second instinct is to build a separate native screen and navigate to it from Flutter. This works but the user experience is often clunky. The transition feels wrong and you lose the power of Flutter’s composable UI.

There is a third way that is usually the right one. You can embed the native UI component directly inside your Flutter widget tree. This feature is called Platform Views. It feels a bit like magic but the idea is simple. Flutter can reserve a piece of the screen and tell the native platform to draw its own view there.

The Native Side

Let’s start with the native code. You need to create two things a factory and the view itself. The factory’s job is to create new instances of your native view when Flutter asks for one.

On iOS using Swift this looks surprisingly simple. You create a factory that conforms to FlutterPlatformViewFactory and a view object that conforms to FlutterPlatformView.

// In your AppDelegate.swift

import UIKit
import Flutter

// 1. The view itself
class SimpleNativeView: NSObject, FlutterPlatformView {
    private var _view: UIView

    init(
        frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?,
        binaryMessenger messenger: FlutterBinaryMessenger?
    ) {
        _view = UIView()
        super.init()
        // Create a label and add it to the view
        let label = UILabel()
        label.text = “This is a native UILabel”
        label.textColor = UIColor.blue
        label.frame = CGRect(x: 0, y: 0, width: 200, height: 50)
        _view.addSubview(label)
    }

    func view() -> UIView {
        return _view
    }
}

// 2. The factory that creates the view
class SimpleNativeViewFactory: NSObject, FlutterPlatformViewFactory {
    private var messenger: FlutterBinaryMessenger

    init(messenger: FlutterBinaryMessenger) {
        self.messenger = messenger
        super.init()
    }

    func create(
        withFrame frame: CGRect,
        viewIdentifier viewId: Int64,
        arguments args: Any?
    ) -> FlutterPlatformView {
        return SimpleNativeView(
            frame: frame,
            viewIdentifier: viewId,
            arguments: args,
            binaryMessenger: messenger
        )
    }
}

// 3. Register the factory
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    guard let controller = window?.rootViewController as? FlutterViewController else {
      fatalError(“rootViewController is not type FlutterViewController”)
    }

    let viewFactory = SimpleNativeViewFactory(messenger: controller.binaryMessenger)
    // The “simple-native-view” string is a unique ID we’ll use in Dart
    self.registrar(forPlugin: “simple-native-view-plugin”)?.register(
        viewFactory,
        withId: “simple-native-view”
    )

    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

The process is very similar on Android using Kotlin. You create a factory and a view.

// In your MainActivity.kt

package com.example.my_app

import android.content.Context
import android.graphics.Color
import android.view.View
import android.widget.TextView
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory
import io.flutter.plugin.common.StandardMessageCodec

// 1. The view itself
internal class SimpleNativeView(context: Context, id: Int, creationParams: Map<String?, Any?>?) : PlatformView {
    private val textView: TextView

    override fun getView(): View {
        return textView
    }

    override fun dispose() {}

    init {
        textView = TextView(context)
        textView.textSize = 16f
        textView.text = “This is a native TextView”
        textView.setBackgroundColor(Color.rgb(255, 255, 255))
        textView.setTextColor(Color.rgb(0, 0, 255))
    }
}

// 2. The factory that creates the view
class SimpleNativeViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
        val creationParams = args as Map<String?, Any?>?
        return SimpleNativeView(context, viewId, creationParams)
    }
}

// 3. Register the factory
class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        flutterEngine
            .platformViewsController
            .registry
            // The “simple-native-view” string is the same unique ID
            .registerViewFactory(“simple-native-view”, SimpleNativeViewFactory())
    }
}

This is all the native code you need. Most of it is boilerplate you write once. The important part is the unique ID simple-native-view. This is how Flutter finds your factory.

The Dart Side

Using your new native view in Flutter is the easy part. You use the UiKitView widget for iOS and the AndroidView widget for Android.

You give them the same unique ID you used when registering the factory.

// main.dart

import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart'; // For defaultTargetPlatform
import 'package:flutter/services.dart'; // For StandardMessageCodec

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Platform View Demo'),
        ),
        body: Center(
          child: SizedBox(
            width: 200,
            height: 50,
            child: _buildNativeView(),
          ),
        ),
      ),
    );
  }

  Widget _buildNativeView() {
    // This is the unique identifier you used on the native side
    const String viewType = 'simple-native-view';
    // You can pass arguments to the native view during creation
    final Map<String, dynamic> creationParams = <String, dynamic>{};

    switch (defaultTargetPlatform) {
      case TargetPlatform.android:
        return AndroidView(
          viewType: viewType,
          layoutDirection: TextDirection.ltr,
          creationParams: creationParams,
          creationParamsCodec: const StandardMessageCodec(),
        );
      case TargetPlatform.iOS:
        return UiKitView(
          viewType: viewType,
          layoutDirection: TextDirection.ltr,
          creationParams: creationParams,
          creationParamsCodec: const StandardMessageCodec(),
        );
      default:
        throw UnsupportedError('Unsupported platform view');
    }
  }
}

And that’s it. You now have a native view rendering inside a Flutter widget. You can wrap it in a SizedBox or any other layout widget. It participates in Flutter’s layout system just like any other widget.

The Performance Cost

This seems too easy. There must be a catch. And there is one a performance cost.

Platform Views are not cheap. They break Flutter’s single canvas rendering model. Every time a platform view is on screen Flutter has to composite its own canvas with the native view hierarchy. This is more work for the GPU and can reduce your frame rate if used incorrectly.

The rule of thumb is to use platform views sparingly. They are perfect for complex components that are hard to replicate in Dart like maps or web views. They are not for simple things like buttons or text fields.

You should also be careful about putting Flutter widgets on top of platform views or animating them frequently. The cost of composition adds up. But for showing a complex isolated native component they are the best tool for the job. They save you from a complete rewrite or a poor user experience.

Using platform views is a trade off between development time and rendering performance. For many apps that need to integrate a specific native SDK it is the right trade off to make. What existing native components could simplify your own work? Take a moment to think about it for your own app.

— Rishi Banerjee
September 2025