← Articles

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:

DartAndroid (Java/Kotlin)iOS (Swift/Obj-C)
nullnullnil
boolBooleanNSNumber(bool)
intInteger/LongNSNumber(int)
doubleDoubleNSNumber(double)
StringStringNSString
ListArrayListNSArray
MapHashMapNSDictionary
Uint8Listbyte[]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.

Flutter platform channels explained — ANN Tech