← Articles

Flutter platform channels explained

By Charlin Joe · 9 April 20250 views

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.