Working with PlatformView for Embedding Native UI Components in Flutter

Flutter excels at providing a cross-platform UI toolkit, enabling developers to build natively compiled applications from a single codebase. However, there are situations where you might want to incorporate native UI components directly into your Flutter app. This is where PlatformView comes into play. PlatformView allows you to embed native Android and iOS views within your Flutter application.

What is PlatformView?

PlatformView is a Flutter mechanism that lets you render platform-specific (native Android or iOS) UI components within a Flutter application. It’s essentially a bridge between Flutter’s rendering pipeline and the native platform’s UI rendering capabilities.

Why Use PlatformView?

  • Leverage Native Components: Integrate native features and UI components not yet available in Flutter.
  • Performance: For computationally heavy or graphically intensive UI tasks, native components may offer better performance.
  • Access to Native APIs: Use native APIs directly through the platform view.
  • Existing Native Codebases: Integrate existing native UI components with minimal rewriting.

How to Implement PlatformView in Flutter

Implementing PlatformView involves several steps:

Step 1: Set Up Platform-Specific Code

First, you need to create the native view in either Android (Java/Kotlin) or iOS (Swift/Objective-C).

Android (Kotlin)

import android.content.Context
import android.view.View
import android.widget.TextView

import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler

class NativeTextView(context: Context, id: Int, creationParams: Map<String, Any?>?) : PlatformView, MethodCallHandler {
    private val textView: TextView = TextView(context)
    private val channel: MethodChannel

    init {
        textView.text = "Native Text View (Android)"
        channel = MethodChannel(FlutterBoost.instance().engineProvider.dartExecutor, "native_text_view_$id")
        channel.setMethodCallHandler(this)

        creationParams?.let {
            val text : String? = it["text"] as? String
            text?.let {
                textView.text = it
            }
        }
    }

    override fun getView(): View {
        return textView
    }

    override fun dispose() {}

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "setText" -> {
                val text = call.arguments as? String
                text?.let {
                    textView.text = it
                    result.success(null)
                } ?: result.error("ARGUMENT_ERROR", "Text cannot be null", null)
            }
            else -> result.notImplemented()
        }
    }
}

This Kotlin code creates a simple TextView.

iOS (Swift)

import Flutter
import UIKit

class NativeTextViewFactory: 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 NativeTextView(frame: frame, viewId: viewId, args: args, messenger: messenger)
    }
}

class NativeTextView: NSObject, FlutterPlatformView {
    private var _view: UIView

    init(
        frame: CGRect,
        viewId: Int64,
        args: Any?,
        messenger: FlutterBinaryMessenger
    ) {
        _view = UIView()
        super.init()
        createNativeView(view: _view, args: args)
    }

    func view() -> UIView {
        return _view
    }

    func createNativeView(view _view: UIView, args: Any?){
        let nativeLabel = UILabel(frame: _view.bounds)
        nativeLabel.text = "Native Text View (iOS)"
        nativeLabel.textAlignment = .center

        if let arguments = args as? [String: Any],
           let text = arguments["text"] as? String {
            nativeLabel.text = text
        }

        _view.addSubview(nativeLabel)
    }
}

This Swift code creates a simple UILabel wrapped in a UIView.

Step 2: Register the Platform View Factory

You need to register the platform view factory in your platform-specific code.

Android (Kotlin) – In your MainActivity.kt or Flutter Plugin

import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.android.FlutterActivity

class MainActivity: FlutterActivity() {
    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        flutterEngine
            .platformViewsController
            .registry
            .registerViewFactory("native_text_view", NativeTextViewFactory(this))
    }
}

class NativeTextViewFactory(private val context: Context): PlatformViewFactory(StandardMessageCodec.INSTANCE) {
    override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
        val creationParams = args as? Map<String, Any?>
        return NativeTextView(context, viewId, creationParams)
    }
}
iOS (Swift) – In your AppDelegate.swift

import UIKit
import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
    let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
    let nativeViewFactory = NativeTextViewFactory(messenger: controller.binaryMessenger)
    controller.registrar(forPlugin: "native_text_view")!.register(
        nativeViewFactory,
        withId: "native_text_view")

    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
}

Step 3: Use PlatformView in Flutter

Now, use the PlatformView widget in your Flutter code:


import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'dart:io' show Platform;

class PlatformTextView extends StatelessWidget {
  final String text;

  const PlatformTextView({Key? key, required this.text}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    String viewType = 'native_text_view';
    // Pass parameters to the platform view.
    final Map<String, dynamic> creationParams = <String, dynamic>{};
    creationParams["text"] = text;

    if (Platform.isAndroid) {
      return PlatformViewLink(
        viewType: viewType,
        surfaceFactory: (context, controller) {
          return AndroidViewSurface(
            controller: controller as AndroidViewController,
            gestureRecognizers: const 

Usage:


import 'package:flutter/material.dart';
import 'platform_text_view.dart';

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: Text('PlatformView Example'),
        ),
        body: Center(
          child: PlatformTextView(text: "Hello from Flutter!"),
        ),
      ),
    );
  }
}

This code uses the PlatformView widget to display the native text view in both Android and iOS. The platform-specific view is selected based on the running platform.

Step 4: Communicate Between Flutter and Native Code (Optional)

You can set up method channels to communicate between Flutter and the native view. In the examples above, a method channel has been implemented.

To invoke the Method Channel from Flutter, the following can be performed:


import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class NativeTextView extends StatefulWidget {
  @override
  _NativeTextViewState createState() => _NativeTextViewState();
}

class _NativeTextViewState extends State<NativeTextView> {
  static const platform = MethodChannel('native_text_view');

  Future<void> _setText(String text) async {
    try {
      await platform.invokeMethod('setText', {'text': text});
    } on PlatformException catch (e) {
      print("Failed to set text: '${e.message}'.");
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: <Widget>[
        // Existing Native View Implementation using PlatformView
        ElevatedButton(
          child: Text('Update Text'),
          onPressed: () {
            _setText("Text Updated from Flutter!");
          },
        ),
      ],
    );
  }
}

In this Dart code:

  • A MethodChannel is set up to invoke a native method
  • The Flutter-side invokes this MethodChannel upon button press.

Considerations

  • Performance Overhead: PlatformView can introduce performance overhead due to the context switching between Flutter and native environments.
  • Complexity: Implementing PlatformView requires writing and maintaining platform-specific code.
  • Accessibility: Make sure your native views properly implement accessibility features.
  • Widget Lifecycle: Native views have their own lifecycle. It is necessary to correctly handle lifecycle events.

Conclusion

PlatformView offers a powerful way to incorporate native UI components into your Flutter application, providing flexibility and access to platform-specific features. While it introduces some complexity and overhead, it is an invaluable tool when you need to leverage native capabilities. Carefully consider your requirements and weigh the benefits against the costs when deciding whether to use PlatformView. Understanding the setup and use of this mechanism enables Flutter developers to deliver feature-rich and high-performance apps by combining Flutter's advantages with native strengths.