Platform Channels & Native Integration

MethodChannel: Android Implementation in Kotlin

16 min Lesson 3 of 11

MethodChannel: Android Implementation in Kotlin

When Flutter needs to call native Android functionality — such as reading device battery level, accessing the camera hardware directly, or invoking a platform SDK — you register a MethodChannel handler on the Android side. This lesson covers how to implement that handler inside a FlutterActivity (or a FlutterPlugin) using Kotlin, wire it up correctly, and return results or structured errors back to Dart.

Where Android-Side Code Lives

In a standard Flutter project the Android host code resides in android/app/src/main/kotlin/…/MainActivity.kt. The entry point is a class that extends FlutterActivity. You override configureFlutterEngine to gain access to the FlutterEngine and register your channels there.

Note: configureFlutterEngine is the correct lifecycle hook for registering channels. Avoid doing this in onCreate, because the engine may not be fully initialised at that point.

Registering a MethodChannel in MainActivity.kt

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {

    private val CHANNEL = "com.example.myapp/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        MethodChannel(
            flutterEngine.dartExecutor.binaryMessenger,
            CHANNEL
        ).setMethodCallHandler { call, result ->
            when (call.method) {
                "getBatteryLevel" -> {
                    val batteryLevel = getBatteryLevel()
                    if (batteryLevel != -1) {
                        result.success(batteryLevel)
                    } else {
                        result.error(
                            "UNAVAILABLE",
                            "Battery level not available.",
                            null
                        )
                    }
                }
                else -> result.notImplemented()
            }
        }
    }
}

The MethodCallHandler Lambda

The trailing lambda passed to setMethodCallHandler receives two parameters every time Dart invokes the channel:

  • call: MethodCall — contains call.method (the String name) and call.arguments (any value sent from Dart, typed as Any?).
  • result: MethodChannel.Result — the callback object you use to reply exactly once: result.success(value), result.error(code, message, details), or result.notImplemented().
Warning: You must call exactly one of success, error, or notImplemented for every invocation. Calling none leaves the Dart Future pending forever; calling more than one throws a runtime exception.

Extracting Arguments Safely

Dart can pass arguments as a Map, a List, or a primitive. On the Kotlin side, cast call.arguments to the expected type. Use the typed helper call.argument<T>(key) for map arguments to avoid unsafe casts.

Handling Arguments and Returning a Result

"greet" -> {
    // Dart sent: {'name': 'Flutter'}
    val name: String? = call.argument<String>("name")
    if (name == null) {
        result.error("MISSING_ARG", "Argument 'name' is required.", null)
        return@setMethodCallHandler
    }
    result.success("Hello from Kotlin, $name!")
}

"multiply" -> {
    val a: Int? = call.argument<Int>("a")
    val b: Int? = call.argument<Int>("b")
    if (a == null || b == null) {
        result.error("MISSING_ARG", "Both 'a' and 'b' are required.", null)
        return@setMethodCallHandler
    }
    result.success(a * b)
}

Implementing as a FlutterPlugin

For reusable native code (e.g., a library distributed via pub.dev), implement the FlutterPlugin interface instead of embedding logic inside MainActivity. This separates the channel registration from the host activity and enables the plugin to be auto-registered.

  • Implement FlutterPlugin and override onAttachedToEngine and onDetachedFromEngine.
  • Register the channel in onAttachedToEngine using the provided FlutterPluginBinding.
  • Unregister (set handler to null) in onDetachedFromEngine to prevent memory leaks.

FlutterPlugin Registration Pattern

import io.flutter.embedding.engine.plugins.FlutterPlugin
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

class BatteryPlugin : FlutterPlugin, MethodCallHandler {

    private lateinit var channel: MethodChannel

    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel = MethodChannel(
            binding.binaryMessenger,
            "com.example.myapp/battery"
        )
        channel.setMethodCallHandler(this)
    }

    override fun onMethodCall(call: MethodCall, result: Result) {
        when (call.method) {
            "getBatteryLevel" -> result.success(getNativeBatteryLevel())
            else              -> result.notImplemented()
        }
    }

    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
        channel.setMethodCallHandler(null) // avoid memory leaks
    }

    private fun getNativeBatteryLevel(): Int {
        // ... platform implementation
        return 87
    }
}

Threading Considerations

By default, MethodCallHandler is invoked on the main (UI) thread. If your native work is long-running (file I/O, network, Bluetooth), offload it to a background thread and then call result.success() back on the main thread using a Handler(Looper.getMainLooper()).post { … } wrapper, or use Kotlin coroutines with Dispatchers.Main for the reply.

Tip: Channel names must match exactly between Dart and Kotlin — including capitalisation and every slash. A common bug is a trailing slash or a typo that silently sends all calls to notImplemented. Define the name in a shared constant or keep it visible in both files for easy comparison.

Summary

To implement a MethodChannel handler on Android: override configureFlutterEngine in MainActivity (or implement FlutterPlugin for reusable plugins); construct a MethodChannel with the binary messenger and a matching channel name; supply a setMethodCallHandler lambda that dispatches on call.method; reply with exactly one of result.success, result.error, or result.notImplemented; and clean up by setting the handler to null when the engine detaches.