Platform Channels

High PriorityAsked in ~75% of senior interviews

5 min read

Platform Integration

ChannelDirectionUse for
MethodChannelDart ↔ NativeSingle request/response — call native API, get value back
EventChannelNative → Dart (stream)Sensor data, location updates, observable system state
BasicMessageChannelDart ↔ Native (custom codec)Binary data, custom serialisation

Codecs (serialization):

CodecSupports
StandardMessageCodec (default)int, double, String, bool, Uint8List, List, Map
JSONMessageCodecJSON-encoded primitives
BinaryCodecRaw ByteData
StringCodecUTF-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

NeedTool
One-off native API call (battery, share, contacts)MethodChannel
Continuous events (sensors, GPS, BLE)EventChannel
Many endpoints with typed arguments and return typesPigeon (codegen on top of MethodChannel)
Sync calls into C/C++ for performanceFFI (Q62)
Rendering a native view inside FlutterPlatformView (AndroidView/UiKitView)
Plugin-style reusable wrapperFederated 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

  1. 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.

  2. MethodChannel vs Pigeon — what's the difference? MethodChannel is 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.

  3. 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.

  4. 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 result when done.


How helpful was this content?

Please sign in to rate this article.