Implementing Platform Channels in Flutter for Native Integration
Learn how to bridge Flutter and native code using MethodChannel, EventChannel, and BasicMessageChannel. Includes a full battery-level example and critical threading guidelines.
28 Feb 2026, 17:37 UTC

The Core Problem: Bridging Dart and Native APIs
Flutter provides a rich UI framework, but it cannot directly access hardware-specific APIs or OS-level services—such as battery status, sensor data, or secure storage—that are only available via Kotlin/Java (Android) or Swift/Objective-C (iOS). To bridge this gap, Flutter uses Platform Channels.
The takeaway is that you must choose the channel type based on the communication pattern: use MethodChannel for one-off requests (RPC style), EventChannel for continuous data streams, and BasicMessageChannel for arbitrary message passing.
Choosing the Right Channel Pattern
| Channel Type | Communication Pattern | Typical Use Case |
|---|---|---|
| MethodChannel | Request-Response (Async) | Fetching a device ID or triggering a haptic vibration. |
| EventChannel | Stream (Native to Dart) | Listening to accelerometer changes or connectivity status. |
| BasicMessageChannel | Bidirectional Messages | Sending raw byte buffers or configuration strings. |
Worked Example: Fetching Battery Level
This example implements a MethodChannel to retrieve the device battery level. To avoid naming collisions, always use a reverse-domain prefix for the channel name.
1. Dart Implementation
Run this code within your Flutter widget or service layer. The invokeMethod call is asynchronous and returns a Future.
import 'package:flutter/services.dart';
class BatteryService {
// Use a unique identifier to prevent collisions
static const platform = MethodChannel('com.example.app/battery');
Future<int> getBatteryLevel() async {
try {
// The method name 'getBatteryLevel' must match the native handler
final int result = await platform.invokeMethod('getBatteryLevel');
return result;
} on PlatformException catch (e) {
print("Failed to get battery level: ${e.message}");
return -1;
}
}
}
2. Android Implementation (Kotlin)
Modify MainActivity.kt. Ensure you are implementing the MethodCallHandler interface. This code runs on the Android main thread.
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.app/battery"
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler {
call, result ->
if (call.method == "getBatteryLevel") {
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level could not be retrieved", null)
}
} else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
// Logic to interface with Android BatteryManager would go here
return 75 // Mocked value
}
}
3. iOS Implementation (Swift)
Modify AppDelegate.swift. The channel is registered during the application launch sequence.
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) → Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "com.example.app/battery",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler { (call, result) in
if (call.method == "getBatteryLevel") {
result(75) // Mocked value
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Critical Engineering Constraints
Threading and Blocking
Platform channel handlers execute on the platform main thread (UI thread). If you perform a heavy computation or a synchronous network request inside the handler, the entire app UI will freeze.
- Android: Use Kotlin Coroutines or
Executorto move work to a background thread, then callresult.success()back on the main thread. - iOS: Use
DispatchQueue.global().asyncfor background work andDispatchQueue.main.asyncto return the result.
Data Type Limitations
The StandardMethodCodec handles basic types (int, bool, String, List, Map). However, it does not support complex objects like DateTime or custom classes. To pass these, you must serialize them: convert a DateTime to an ISO-8601 string or a millisecond timestamp before sending.
Memory Leaks in EventChannels
When using EventChannel, the native side must implement onCancel. If you fail to release native listeners or stop sensors in the onCancel callback, the native resource will leak even after the Dart subscription is cancelled.
Verification and Testing
To verify the implementation without a physical device for every change, use the TestDefaultBinaryMessengerBinding in Dart unit tests. This allows you to stub the native response:
// In a Dart test file
TestDefaultBinaryMessengerBinding.instance.setMockMethodCallHandler(
'com.example.app/battery',
(methodCall) async {
if (methodCall.method == 'getBatteryLevel') return 80;
return null;
}
);
For production verification, run flutter run --verbose. This will output the registration of platform channels in the logs, allowing you to confirm that the channel name in Dart matches the name registered in the native MainActivity or AppDelegate.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.