Platform Channels
5 min read
Platform Integration
| Channel | Direction | Use for |
|---|---|---|
MethodChannel | Dart ↔ Native | Single request/response — call native API, get value back |
EventChannel | Native → Dart (stream) | Sensor data, location updates, observable system state |
BasicMessageChannel | Dart ↔ Native (custom codec) | Binary data, custom serialisation |
Codecs (serialization):
| Codec | Supports |
|---|---|
StandardMessageCodec (default) | int, double, String, bool, Uint8List, List, Map |
JSONMessageCodec | JSON-encoded primitives |
BinaryCodec | Raw ByteData |
StringCodec | UTF-8 strings only |
All channel calls are asynchronous and cross the platform thread — never block on the UI thread.
Code in action — battery level (the canonical example)
// Dart side
class BatteryService {
static const _channel = MethodChannel('com.app.battery');
Future<int> getBatteryLevel() async {
try {
final level = await _channel.invokeMethod<int>('getBatteryLevel');
return level ?? -1;
} on PlatformException catch (e) {
throw BatteryException('${e.code}: ${e.message}');
}
}
}
// EventChannel — observe sensor stream
class AccelerometerService {
static const _channel = EventChannel('com.app.accelerometer');
Stream<AccelData> get stream => _channel
.receiveBroadcastStream()
.map((e) => AccelData.fromMap(e as Map));
}
// Android (Kotlin)
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(engine: FlutterEngine) {
super.configureFlutterEngine(engine)
MethodChannel(engine.dartExecutor.binaryMessenger, "com.app.battery")
.setMethodCallHandler { call, result ->
when (call.method) {
"getBatteryLevel" -> {
val mgr = getSystemService(BATTERY_SERVICE) as BatteryManager
result.success(mgr.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY))
}
else -> result.notImplemented()
}
}
}
}
// iOS (Swift)
override func application(_ app: UIApplication,
didFinishLaunchingWithOptions opts: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let channel = FlutterMethodChannel(name: "com.app.battery",
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler { call, result in
if call.method == "getBatteryLevel" {
UIDevice.current.isBatteryMonitoringEnabled = true
result(Int(UIDevice.current.batteryLevel * 100))
} else {
result(FlutterMethodNotImplemented)
}
}
return super.application(app, didFinishLaunchingWithOptions: opts)
}
When to reach for each tool
| Need | Tool |
|---|---|
| One-off native API call (battery, share, contacts) | MethodChannel |
| Continuous events (sensors, GPS, BLE) | EventChannel |
| Many endpoints with typed arguments and return types | Pigeon (codegen on top of MethodChannel) |
| Sync calls into C/C++ for performance | FFI (Q62) |
| Rendering a native view inside Flutter | PlatformView (AndroidView/UiKitView) |
| Plugin-style reusable wrapper | Federated plugin with platform interface package |
Common mistakes to avoid
❌ Not handling PlatformException
`invokeMethod` throws on the platform side — without try/catch you get an
uncaught error in production.
❌ Calling result.success() AND result.error() in the same handler
Each handler MUST call result exactly once. Double-resolve = crash.
❌ Heavy work on the platform's main thread
The native handler runs on the UI thread. Blocking it stalls Flutter too.
✅ Offload long work to a background dispatch queue / coroutine.
❌ Hand-rolling MethodChannel where Pigeon would do
Pigeon gives you typed messages, generated code, fewer string-keyed bugs.
❌ Sending huge payloads through StandardMessageCodec
Each call copies the payload. For large bytes, consider FFI or shared
buffers (PlatformView, image bytes via Uint8List view).
❌ Forgetting EventChannel cleanup
Stream subscribers must be cancelled in dispose() — otherwise native side
keeps emitting (and holding callbacks) forever.
Interview follow-ups
-
How would you test platform-channel code? Mock the channel:
_channel.setMockMethodCallHandler((call) async => ...)in tests so your Dart code thinks it's talking to the platform. For end-to-end coverage, use integration tests on a real device or emulator. Pigeon-generated APIs are easier — you mock the generated interface, not raw strings. -
MethodChannel vs Pigeon — what's the difference?
MethodChannelis hand-coded: string method names, untyped arguments, manual codec. Pigeon generates typed Dart + Swift + Kotlin from one schema file — you get compile-time safety, IDE autocompletion, no string typos. For anything beyond a couple of methods, Pigeon is the right default. -
What's the performance cost of a channel call? A few hundred microseconds typically — copying through the codec, hopping threads, going through the platform message queue. Fine for occasional calls. For per-frame or high-frequency calls (audio, ML), use FFI.
-
What thread do platform handlers run on? Android: by default, the platform thread (NOT the main/UI thread, though both are involved). iOS: the main thread. Either way, don't block — dispatch heavy work to a background thread/queue and call
resultwhen done.
How helpful was this content?
Please sign in to rate this article.