How to Add Native Code to a Flutter App using Platform Views on Android

Step-by-step guide about how to add native code and native components to a Flutter app

In this tutorial, we learn how to add native components to a Flutter app on Android. For iOS the process is similar, but this tutorial does not cover it. We are going to add a native WebView component as an example, and we write code in Kotlin.

Step 1. Define a wrapper component in Flutter

The component will be used throughout your app and the communication with native code will be encapsulated within. In this tutorial, we create a WebView component as follows:

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

typedef void WebViewCreatedCallback(WebViewController controller);

class WebView extends StatefulWidget {
  const WebView({
    Key key,
    this.onWebViewCreated,
  }) : super(key: key);

  final WebViewCreatedCallback onWebViewCreated;

  @override
  State<StatefulWidget> createState() => WebViewState();
}

class WebViewState extends State<WebView> {
  @override
  Widget build(BuildContext context) {
    if (defaultTargetPlatform == TargetPlatform.android) {
      return AndroidView(
        viewType: 'webview',
        onPlatformViewCreated: _onPlatformViewCreated,
      );
    }
    // TODO add other platforms
    return Text(
        '$defaultTargetPlatform is not yet supported by the map view plugin');
  }

  void _onPlatformViewCreated(int id) {
    if (widget.onWebViewCreated == null) {
      return;
    }
    widget.onWebViewCreated(new WebViewController(id));
  }
}

class WebViewController {
  WebViewController(int id) {
    this._channel = new MethodChannel('webview$id');
  }

  MethodChannel _channel;

  Future<void> loadUrl(String url) async {
    return _channel.invokeMethod('loadUrl', url);
  }
}

In this code, we declare WebView as a stateful widget. The state of the widget is defined in WebViewState. In the build method we check that the platform is Android, and create an AndroidView. viewType is needed to be able to connect the native code to this AndroidView. Once the view is created, we instantiate WebViewController and to give control over the view to the parent code.

In WebViewController we create a method channel which has a unique name for every instance of the component. Communication between native code and Flutter happens via bidirectional async method channels. In loadUrl we send a message to a channel to invoke the loadUrl method of the native component and give a URL to load.

Step 2. Define native component

Go to the android -> app -> src -> main -> kotlin -> com -> yourPackageName. This is where we place out native code. First, we define a native plugin in WebViewPlugin.kt:

import io.flutter.plugin.common.PluginRegistry.Registrar

object WebViewPlugin {
    fun registerWith(registrar: Registrar) {
        registrar
                .platformViewRegistry()
                .registerViewFactory(
                        "webview", WebViewFactory(registrar.messenger()))
    }
}

The string webview here should match the viewType used in the previous step. This code tells the app that WebViewFactory is used to create instances for this view type.

Next, we define the factory in WebViewFactory.kt:

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

class WebViewFactory(private val messenger: BinaryMessenger) : PlatformViewFactory(StandardMessageCodec.INSTANCE) {

    override fun create(context: Context, id: Int, o: Any?): PlatformView {
        return MyWebView(context, messenger, id)
    }
}

The factory creates instances of MyWebView which is our native implementation of the component. We define it in MyWebView.kt:

import android.content.Context
import android.view.View
import android.widget.TextView
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.platform.PlatformView
import android.webkit.*
import android.net.http.SslError
import android.webkit.WebChromeClient
import android.webkit.WebView
import android.graphics.Bitmap
import android.webkit.WebViewClient

class MyWebView internal constructor(context: Context, messenger: BinaryMessenger, id: Int) : PlatformView, MethodCallHandler {
    private val webView: WebView
    private val methodChannel: MethodChannel

    override fun getView(): View {
        return webView
    }

    init {
        webView = WebView(context)

        methodChannel = MethodChannel(messenger, "webview$id")
        methodChannel.setMethodCallHandler(this)
    }

    override fun onMethodCall(methodCall: MethodCall, result: MethodChannel.Result) {
        when (methodCall.method) {
            "loadUrl" -> loadUrl(methodCall, result)
            else -> result.notImplemented()
        }
    }

    private fun loadUrl(methodCall: MethodCall, result: Result) {
        val url = methodCall.arguments as String
        webView.loadUrl(url)
        result.success(null)
    }

    override fun dispose() {
      // TODO dispose actions if needed
    }
}

In this class, we create the native WebView provided by Android and a method channel. We use the same name for the method channel so that messages are routed to the correct Flutter instance.

We also dispatch different calls and handle loadUrl. This class is the place where you can implement additional features. For example, configure the webview for your needs.

Step 3. Register native code

The final step is to modify android -> app -> src -> main -> kotlin -> com -> yourPackageName -> MainActivity.kt. We add a registration call to register our plugin:

import android.os.Bundle

import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant

class MainActivity: FlutterActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    GeneratedPluginRegistrant.registerWith(this)
    WebViewPlugin.registerWith(this.registrarFor("com.yourPackageName"))
  }
}

Now the plugin is complete, and you can use it in your Flutter code.