MethodChannel: Android Implementation in Kotlin
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.
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— containscall.method(the String name) andcall.arguments(any value sent from Dart, typed asAny?).result: MethodChannel.Result— the callback object you use to reply exactly once:result.success(value),result.error(code, message, details), orresult.notImplemented().
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
FlutterPluginand overrideonAttachedToEngineandonDetachedFromEngine. - Register the channel in
onAttachedToEngineusing the providedFlutterPluginBinding. - Unregister (set handler to
null) inonDetachedFromEngineto 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.
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.