Flutter platform channels explained
By Charlin Joe · 9 April 2025
Platform channels are Flutter's bridge to native code. When you need to call an Android or iOS API that has no Flutter equivalent, you send a message across a platform channel and receive the result back in Dart. Here is how they work and how to use them correctly.
The three channel types
- MethodChannel: one-off calls from Dart to native. Best for: permissions, device APIs, one-shot operations.
- EventChannel: continuous stream of events from native to Dart. Best for: sensor data, Bluetooth events, location updates.
- BasicMessageChannel: bidirectional message passing with custom codecs. Rarely needed — use MethodChannel or EventChannel.
MethodChannel: Dart to native
// Dart side
const _channel = MethodChannel('com.example.myapp/device');
Future<String?> getDeviceId() async {
try {
return await _channel.invokeMethod<String>('getDeviceId');
} on PlatformException catch (e) {
debugPrint('getDeviceId failed: ${e.message}');
return null;
}
}
// Pass arguments
Future<bool> setBrightness(double level) async {
return await _channel.invokeMethod<bool>('setBrightness', {
'level': level,
}) ?? false;
}
Android implementation (Kotlin)
// In MainActivity.kt
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(
flutterEngine.dartExecutor.binaryMessenger,
"com.example.myapp/device"
).setMethodCallHandler { call, result ->
when (call.method) {
"getDeviceId" -> {
val id = Settings.Secure.getString(
contentResolver,
Settings.Secure.ANDROID_ID
)
result.success(id)
}
"setBrightness" -> {
val level = call.argument<Double>("level") ?: 0.5
// Set brightness...
result.success(true)
}
else -> result.notImplemented()
}
}
}
}
iOS implementation (Swift)
// In AppDelegate.swift
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller = window?.rootViewController as! FlutterViewController
FlutterMethodChannel(
name: "com.example.myapp/device",
binaryMessenger: controller.binaryMessenger
).setMethodCallHandler { call, result in
switch call.method {
case "getDeviceId":
result(UIDevice.current.identifierForVendor?.uuidString)
case "setBrightness":
let args = call.arguments as! [String: Any]
let level = args["level"] as! Double
UIScreen.main.brightness = CGFloat(level)
result(true)
default:
result(FlutterMethodNotImplemented)
}
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
EventChannel: native to Dart streams
// Dart
const _sensorChannel = EventChannel('com.example.myapp/accelerometer');
Stream<AccelerometerData> get accelerometerStream {
return _sensorChannel
.receiveBroadcastStream()
.map((event) {
final map = event as Map<Object?, Object?>;
return AccelerometerData(
x: (map['x'] as num).toDouble(),
y: (map['y'] as num).toDouble(),
z: (map['z'] as num).toDouble(),
);
});
}
// Android: EventChannel with SensorEventListener
class AccelerometerStreamHandler : EventChannel.StreamHandler {
private var eventSink: EventChannel.EventSink? = null
private var sensorManager: SensorManager? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
eventSink = events
// Start sensor...
}
override fun onCancel(arguments: Any?) {
eventSink = null
// Stop sensor...
}
fun onSensorUpdate(x: Float, y: Float, z: Float) {
eventSink?.success(mapOf("x" to x, "y" to y, "z" to z))
}
}
Type mapping
Platform channels serialize Dart types to platform types automatically:
| Dart | Android (Java/Kotlin) | iOS (Swift/Obj-C) |
|---|---|---|
null | null | nil |
bool | Boolean | NSNumber(bool) |
int | Integer/Long | NSNumber(int) |
double | Double | NSNumber(double) |
String | String | NSString |
List | ArrayList | NSArray |
Map | HashMap | NSDictionary |
Uint8List | byte[] | FlutterStandardTypedData |
Error handling
try {
await _channel.invokeMethod('sensitiveOperation');
} on PlatformException catch (e) {
// e.code: error code from native
// e.message: human-readable message
// e.details: any additional data
switch (e.code) {
case 'PERMISSION_DENIED':
showPermissionDialog();
case 'NOT_SUPPORTED':
showUnsupportedFeatureMessage();
default:
reportError(e);
}
} on MissingPluginException {
// Channel handler not registered (e.g., on a platform you didn't implement)
debugPrint('Platform not supported');
}
Pigeon for type safety
Pigeon generates type-safe channel code from a Dart definition:
// pigeons/api.dart
@HostApi()
abstract class DeviceApi {
String getDeviceId();
bool setBrightness(double level);
}
flutter pub run pigeon \
--input pigeons/api.dart \
--dart_out lib/src/device_api.g.dart \
--kotlin_out android/.../DeviceApi.g.kt \
--swift_out ios/Classes/DeviceApi.g.swift
Pigeon eliminates the string-based method names that cause runtime errors on typos.
Common pitfalls
Calling result() more than once. Each method call must call result exactly once. Calling it twice (e.g., once in a callback and once in the main thread) crashes with a FlutterException.
Channel name collisions. If two plugins use the same channel name, one silently shadows the other. Use your reverse domain as the prefix: com.yourcompany.appname/feature.
Calling native from a background isolate. Platform channels only work on the main isolate. Use IsolateNameServer to communicate results from background isolates back to the main isolate.
Sign in to like, dislike, or report.