← Articles

Adding native Android code to a Flutter app

By Charlin Joe · 27 April 2025

Sometimes Flutter's platform channels need Kotlin/Java on Android to access a native API. This guide shows how to add Android native code to a Flutter plugin using method channels.

When you need native code

  • Platform APIs not exposed by any Flutter package (e.g., custom hardware, specific system APIs)
  • Platform-specific capabilities that existing packages don't cover or don't fit your needs
  • Performance-critical code that must run natively
  • Integrating a vendor Android SDK

Method channel basics

A MethodChannel is a bidirectional communication pipe between Flutter (Dart) and the native platform. You invoke methods from Dart; Android handles them in Kotlin.

Dart side:

class BatteryService {
  static const _channel = MethodChannel('com.example.myapp/battery');

  Future<int> getBatteryLevel() async {
    try {
      final level = await _channel.invokeMethod<int>('getBatteryLevel');
      return level ?? -1;
    } on PlatformException catch (e) {
      throw BatteryException('Failed to get battery level: ${e.message}');
    }
  }

  // Send arguments
  Future<bool> setSaverMode(bool enabled) async {
    return await _channel.invokeMethod<bool>(
      'setSaverMode',
      {'enabled': enabled},
    ) ?? false;
  }
}

Kotlin side (android/app/src/main/kotlin/com/example/myapp/MainActivity.kt):

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager

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 level = getBatteryLevel()
            if (level != -1) result.success(level)
            else result.error("UNAVAILABLE", "Battery level not available", null)
          }
          "setSaverMode" -> {
            val enabled = call.argument<Boolean>("enabled") ?: false
            result.success(setSaverMode(enabled))
          }
          else -> result.notImplemented()
        }
      }
  }

  private fun getBatteryLevel(): Int {
    val batteryStatus: Intent? = IntentFilter(Intent.ACTION_BATTERY_CHANGED).let { filter ->
      applicationContext.registerReceiver(null, filter)
    }
    val level = batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1
    val scale = batteryStatus?.getIntExtra(BatteryManager.EXTRA_SCALE, -1) ?: -1
    return if (level != -1 && scale != -1) (level * 100 / scale.toFloat()).toInt() else -1
  }

  private fun setSaverMode(enabled: Boolean): Boolean {
    // Implementation
    return true
  }
}

Event channels (native → Flutter streams)

For continuous data from Android (sensor data, location updates, connectivity changes):

Kotlin:

class MainActivity : FlutterActivity() {
  private val EVENT_CHANNEL = "com.example.myapp/connectivity"
  private var eventSink: EventChannel.EventSink? = null

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

    EventChannel(flutterEngine.dartExecutor.binaryMessenger, EVENT_CHANNEL)
      .setStreamHandler(object : EventChannel.StreamHandler {
        override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
          eventSink = events
          registerConnectivityReceiver()
        }

        override fun onCancel(arguments: Any?) {
          unregisterConnectivityReceiver()
          eventSink = null
        }
      })
  }

  private fun onConnectivityChanged(isConnected: Boolean) {
    activity?.runOnUiThread {
      eventSink?.success(isConnected)
    }
  }
}

Dart:

class ConnectivityService {
  static const _eventChannel = EventChannel('com.example.myapp/connectivity');

  Stream<bool> get connectivityStream =>
      _eventChannel.receiveBroadcastStream()
          .map((event) => event as bool);
}

// Usage:
StreamBuilder<bool>(
  stream: connectivityService.connectivityStream,
  builder: (context, snapshot) {
    if (snapshot.data == false) return const OfflineBanner();
    return const SizedBox.shrink();
  },
)

Adding a third-party Android SDK

  1. Add the dependency in android/app/build.gradle.kts:
dependencies {
  implementation("com.vendorname:their-sdk:1.2.3")
}
  1. Initialize the SDK in MainActivity.onCreate or the Application class:
class MyApplication : FlutterApplication() {
  override fun onCreate() {
    super.onCreate()
    VendorSdk.initialize(this, apiKey = BuildConfig.VENDOR_API_KEY)
  }
}
  1. Register MyApplication in AndroidManifest.xml:
<application
  android:name=".MyApplication"
  ...>

Running code on a background thread

Never block the main thread from native code:

"heavyCompute" -> {
  // Run off main thread
  Thread {
    try {
      val result = doExpensiveWork()
      // Post back to main thread for result.success()
      Handler(Looper.getMainLooper()).post {
        result.success(result)
      }
    } catch (e: Exception) {
      Handler(Looper.getMainLooper()).post {
        result.error("COMPUTE_FAILED", e.message, null)
      }
    }
  }.start()
}

Common pitfalls

Calling result.success() more than once. Each method call gets exactly one response. Calling result.success() or result.error() twice throws an exception. Ensure only one path calls result in each branch.

Channel name mismatch. The channel name string in Dart and Kotlin must be identical, including capitalization. A mismatch results in MissingPluginException with no obvious error message.

Memory leaks from registered receivers. If you register a BroadcastReceiver or listener in onListen, you must unregister in onCancel and also in onDetachedFromActivityForConfigChanges. Forgetting this leaks the activity.

Sign in to like, dislike, or report.

Adding native Android code to a Flutter app — ANN Tech