Flutter Explained: Render Native Controls in Flutter

Tomic Riedel
6 min readMay 27, 2024

--

Icon by flaticon

Flutter draws content onto a texture and its widget tree is self-contained. Because of that, there is no opportunity for Android or iOS views to be part of Flutter’s internal structure or to be rendered alongside Flutter widgets. This challenges developers who want to integrate existing platform components, like a browser control, into their Flutter applications.

Luckily, there is a solution in Flutter.

Platform view widgets.

These widgets allow you to embed this into your own Flutter app.

There are two platform view widgets available: AndroidView and UIKitView.

In today’s article, we will look at how we can integrate these views into our Flutter widget tree.

How Platform Views work and when to use them

Each widget functions as a bridge to the underlying operating system. For example, on Android, the AndroidView widget performs several key roles: it copies the graphic texture from the native view. It integrates it into Flutter by displaying it on a Flutter-rendered surface each time a frame is painted. Additionally, it manages hit testing and input gestures, translating these into the appropriate native inputs. It also builds a version of the accessibility tree, enabling the transfer of commands and responses between the native and Flutter layers.

But this leads to one big problem: This method has some overhead because Android and iOS are being “simulated”. That’s why you using Platform views only makes sense when you want to use something more complex that’s not worth reimplementing in Flutter, e.g. a native video player or, an example from the Flutter documentation, Google Maps.

So, generally speaking:

If it’s not too much overhead to implement it by yourself, don’t use platform views. If it’s something more complicated that would require a lot of your time, use platform views.

Implementing Platform Views

Let’s implement a platform view for Android and iOS. Our initial setup will look like this:

Widget build(BuildContext context) {
// Used in the platform side to register the view
const String viewType = '<platform-view-type>';
// Pass parameters to the platform side
final Map<String, dynamic> creationParams = <String, dynamic>{};

switch (defaultTargetPlatform) {
case TargetPlatform.android:
// Widget for Android.
case TargetPlatform.iOS:
// Widget for iOS.
default:
throw UnsupportedError('Unsupported platform');
}
}

Let’s start by implementing our AndroidView:

Implement AndroidView

There are two ways to implement an Android view.

  1. Hybrid Composition (Platform views are rendered normally, just the Flutter widgets are rendered into textures)
  2. Texture Layer (Platform views are rendered into textures, Flutter content is rendered into a Surface directly)

Each one has its advantages and disadvantages.

While using a Hybrid Composition has a great performance on the Android view, the Flutter performance will go down and your FPS rate will drop.

Using the Texture Layer approach will enhance the performance on both sides, but scrolling fast will be problematic, as well as text magnifiers and SurfaceViews.

For this tutorial, we are going to use the Texture Layer approach, but feel free to visit the Flutter documentation if you want to use the Hybrid Composition approach.

First, add the following imports:

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

In your switch statement, add the following code:

case TargetPlatform.android:
AndroidView(
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams, // Parameters you want to pass to the view
creationParamsCodec: const StandardMessageCodec(), // The format of the parameters
),

You may be asking: What is the StandardMessageCodec class?

It defines how the creation parameters are formatted. On Android, messages are represented as follows:

On Android, messages are represented as follows:

  • null: null
  • bool: java.lang.Boolean
  • int: java.lang.Integer for values that are representable using 32-bit two’s complement; java.lang.Long otherwise
  • double: java.lang.Double
  • String: java.lang.String
  • Uint8List: byte
  • Int32List: int
  • Int64List: long
  • Float64List: double
  • List: java.util.ArrayList
  • Map: java.util.HashMap

Now, to the android side. We are going to implement the code in Kotlin. First, create a new view. I’m just going to call it AndroidView:

package dev.flutter.example

import android.content.Context
import android.graphics.Color
import android.view.View
import android.widget.TextView
import io.flutter.plugin.platform.PlatformView

internal class AndroidView(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)
// Red background
textView.setBackgroundColor(Color.rgb(255, 0, 0))
textView.text = "This text was rendered natively on Android"
}
}

Create a factory class that creates an instance of the AndroidView:

package dev.flutter.example

import android.content.Context
import io.flutter.plugin.common.StandardMessageCodec
import io.flutter.plugin.platform.PlatformView
import io.flutter.plugin.platform.PlatformViewFactory

class AndroidViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) {
override fun create(context: Context, viewId: Int, args: Any?): PlatformView {
val creationParams = args as Map<String?, Any?>?
return AndroidView(context, viewId, creationParams)
}
}

But we haven’t registered the platform view yet. Let’s do this in our MainActivity.kt:

package dev.flutter.example

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

class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
flutterEngine
.platformViewsController
.registry
.registerViewFactory("<platform-view-type>", // The same we specified in our dart code
AndroidViewFactory())
}
}

And that’s it. You now have your Android view in Flutter!

Implement UiKitView

Implementing native iOS code is similar to Android on the Flutter side. Use the same imports as above and add the following to your switch statement:

case TargetPlatform.iOS:
UiKitView(
viewType: viewType,
layoutDirection: TextDirection.ltr,
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
),

Here, we have the StandardMessageCodec again. For iOS, messages are represented as follows:

  • null: nil
  • bool: NSNumber numberWithBool:
  • int: NSNumber numberWithInt: for values that are representable using 32-bit two’s complement; NSNumber numberWithLong: otherwise
  • double: NSNumber numberWithDouble:
  • String: NSString
  • Uint8List, Int32List, Int64List, Float64List: FlutterStandardTypedData
  • List: NSArray
  • Map: NSDictionary
  • int: NSNumber numberWithInt: for values that are representable using 32-bit two’s complement; NSNumber numberWithLong: otherwise
    double: NSNumber numberWithDouble:
    String: NSString
    Uint8List, Int32List, Int64List, Float64List: FlutterStandardTypedData
    List: NSArray
    Map: NSDictionary

We are going to implement our native widget with Swift. First, create your factory and view itself:

import Flutter
import UIKit

class FLUiKitViewFactory: 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 FLUiKitView(
frame: frame,
viewIdentifier: viewId,
arguments: args,
binaryMessenger: messenger)
}

public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol {
return FlutterStandardMessageCodec.sharedInstance()
}
}

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

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

func view() -> UIView {
return _view
}

func createUiKitView(view _view: UIView){
_view.backgroundColor = UIColor.red
let label = UILabel()
label.text = "This text was rendered natively on iOS"
label.textColor = UIColor.yellow
label.textAlignment = .center
label.frame = CGRect(x: 0, y: 0, width: 100, height: 50.0)
_view.addSubview(nativeLabel)
}
}

Now, all we need to do is register our platform view again in AppDelegate.swift:

import Flutter
import UIKit

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

weak var registrar = self.registrar(forPlugin: "plugin-name")

let factory = FLUiKitViewFactory(messenger: registrar!.messenger())
self.registrar(forPlugin: "<plugin-name>")!.register(
factory,
withId: "<platform-view-type>")
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}

And you are done, too. Congratulations!

Conclusion

In this article, you have learned how to render native views in Flutter. If you’ve liked this article, please give it a clap.

Since you have made it to the end, I am you will also like these articles:

PS: This article is heavily based on the Flutter documentation. You can read more about the topic of rendering native views in Flutter here and here.

--

--

Tomic Riedel
Tomic Riedel

Written by Tomic Riedel

Sharing the process of building a portfolio of apps to make people more productive.