Flutter PlatformView: How to host native android and iOS view in Flutter – Detailed Guide

Flutter is a versatile framework for building cross-platform applications, allowing developers to write code once and deploy it on multiple platforms, including Android and iOS. However, there are scenarios where you need to integrate native platform-specific components, such as when utilizing certain native APIs or third-party native libraries. This is where PlatformView comes into play. PlatformView enables you to host native Android Views and iOS UIViews directly within your Flutter application. This guide will provide a comprehensive overview of how to implement PlatformView in Flutter.

What is Flutter PlatformView?

PlatformView is a widget in Flutter that acts as a bridge, allowing you to embed native UI components (Android’s View or iOS’s UIView) directly into your Flutter application’s widget tree. This is particularly useful when you want to leverage specific native features or UI elements that are not readily available or easily replicable in Flutter. By using PlatformView, you can combine the benefits of Flutter’s rapid development with the power and flexibility of native platform capabilities.

Why Use PlatformView?

  • Access to Native Features: Use platform-specific APIs and functionalities directly within your Flutter app.
  • Integration of Native UI Components: Incorporate custom native UI elements or third-party native libraries seamlessly.
  • Performance Optimization: Utilize native components for performance-critical tasks that might be less efficient in Flutter.

How to Implement PlatformView in Flutter

Implementing PlatformView involves creating platform-specific code to define and manage the native view, as well as Flutter code to host it. Let’s go through the process step by step.

Step 1: Setting Up Your Flutter Project

First, create a new Flutter project or navigate to your existing project.

flutter create my_platform_view_app
cd my_platform_view_app

Step 2: Defining the PlatformView Factory in Flutter

You need to define a PlatformViewFactory, which is responsible for creating instances of the native view. This factory is registered with a unique identifier that you’ll use in your Flutter code.

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

class NativeTextView extends StatefulWidget {
  final String text;

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

  @override
  _NativeTextViewState createState() => _NativeTextViewState();
}

class _NativeTextViewState extends State {
  @override
  Widget build(BuildContext context) {
    // This is used in the platform side to register the view.
    const String viewType = 'native_text_view';
    // Pass parameters to the platform side.
    final Map<String, dynamic> creationParams = <String, dynamic>{
      "text": widget.text,
    };

    return PlatformViewLink(
      viewType: viewType,
      surfaceFactory: (context, controller) {
        return AndroidViewSurface(
          controller: controller as AndroidViewController,
          gestureRecognizers: const <Factory>{},
          hitTestBehavior: PlatformViewHitTestBehavior.opaque,
        );
      },
      onCreatePlatformView: (params) {
        return PlatformViewsService.initSurfaceAndroidView(
          id: params.id,
          viewType: viewType,
          layoutDirection: TextDirection.ltr,
          creationParams: creationParams,
          creationParamsCodec: const StandardMessageCodec(),
          onFocus: () {
            params.onFocusChanged(true);
          },
        ) ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
          ..create();
      },
    );
  }
}

Step 3: Implementing the Native View on Android

Navigate to the Android part of your Flutter project (android/app/src/main/kotlin/.../MainActivity.kt) and implement the PlatformViewFactory.

package com.example.my_platform_view_app

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

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

class NativeTextViewFactory(private val messenger: io.flutter.plugin.common.BinaryMessenger): 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?.get("text") as? String ?: "")
    }
}

class NativeTextView(context: Context, id: Int, text: String) : PlatformView {
    private val textView: TextView = TextView(context)

    override fun getView(): View {
        return textView
    }

    init {
        textView.text = text
    }

    override fun dispose() {}
}

Key parts of the Android implementation:

  • NativeTextViewFactory: Creates instances of NativeTextView.
  • NativeTextView: The native Android TextView which will be hosted in Flutter.
  • registerViewFactory: Registers the NativeTextViewFactory with the FlutterEngine.

Step 4: Implementing the Native View on iOS

For iOS, you’ll need to modify the AppDelegate.swift file.

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 factory = NativeTextViewFactory(messenger: controller.binaryMessenger)
    controller.register(
        factory,
        withId: "native_text_view")

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

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, viewIdentifier: viewId, arguments: args, binaryMessenger: messenger)
    }
}

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

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

    func view() -> UIView {
        return _view
    }

    func createNativeView(view _view: UILabel, arguments args: Any?){
        guard let args = args as? [String: Any] else {
            _view.text = "No text";
            return
        }
        _view.text = args["text"] as? String
    }
}

Key points for the iOS implementation:

  • NativeTextViewFactory: Creates instances of NativeTextView for iOS.
  • NativeTextView: Implements the FlutterPlatformView protocol and returns a UILabel.
  • register: Registers the NativeTextViewFactory with the Flutter engine.

Step 5: Using the PlatformView in Flutter

Now you can use the NativeTextView widget in your Flutter UI.

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

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

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter PlatformView Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: Scaffold(
        appBar: AppBar(
          title: const Text('PlatformView Demo'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              const Text(
                'Native Text View:',
                style: TextStyle(fontSize: 20),
              ),
              SizedBox(height: 20),
              SizedBox(
                  width: 300,
                  height: 100,
                  child: NativeTextView(text: "Hello from Native!")
              ),
            ],
          ),
        ),
      ),
    );
  }
}

In this example, the NativeTextView widget is placed within a Column in the Flutter UI. The native text view will render with the provided text.

Advanced Usage

Communication Between Flutter and Native Code

You can also establish communication between your Flutter code and the native PlatformView using MethodChannel. This allows you to send data and invoke methods on the native side.

// Flutter side
import 'package:flutter/services.dart';

class NativeTextView extends StatefulWidget {
  // ...

  @override
  _NativeTextViewState createState() => _NativeTextViewState();
}

class _NativeTextViewState extends State {
  static const MethodChannel methodChannel = MethodChannel('native_text_view_channel');

  @override
  void initState() {
    super.initState();
    // Call a method on the native side
    methodChannel.invokeMethod('setText', {'text': widget.text});
  }

  // ...
}

// Android side
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class NativeTextView(...) : PlatformView, MethodChannel.MethodCallHandler {
    private val methodChannel: MethodChannel = MethodChannel(messenger, "native_text_view_channel")

    init {
        methodChannel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
        when (call.method) {
            "setText" -> {
                val text = call.argument("text") ?: ""
                textView.text = text
                result.success(null)
            }
            else -> {
                result.notImplemented()
            }
        }
    }
    // ...
}

Conclusion

Flutter’s PlatformView provides a robust mechanism for embedding native Android and iOS views into your Flutter application. By following this detailed guide, you can integrate native UI components and leverage platform-specific features, enhancing your app’s functionality and performance. Whether you need to access native APIs, integrate third-party native libraries, or optimize performance-critical tasks, PlatformView allows you to combine the best of both worlds: Flutter’s rapid development and the power of native platforms.