Platform Channels: Integrating Swift and Kotlin Native Code
Flutter is great for 90% of app features, but they exist cases where you need direct access to the platform's native APIs: biometric sensors, Bluetooth LE, Apple Pay, Google Pay, file system access native, integration with third-party SDKs not available as Flutter plugins. For these cases there are Platform Channels.
Platform Channels create a two-way communication bridge between Darts and native code (Swift on iOS, Kotlin on Android). You're not writing Java o Legacy Objective-C: Modern Kotlin and Swift, with coroutines and async/await respectively. In this guide we build a complete plugin that accesses to the device's proximity sensor — not available in Flutter core.
What You Will Learn
- MethodChannel: Dart → native calls with response (request/response)
- EventChannel: Continuous stream of data from native → Dart
- BasicMessageChannel: unstructured bidirectional communication
- Error handling: PlatformException from both directions
- Threading: main thread, background thread, FlutterEngine
- Plugin package: Structure for reusable plugins
- Pigeon: Type-safe code generation for Platform Channels
MethodChannel: Synchronous Request/Response
Il MethodChannel and the most common type of channel: the Dart code calls a native method and waits for a response. Ideal for one-shot operations how to get information about the device, open a native screen, perform a biometric operation.
// lib/proximity_sensor.dart: interfaccia Dart
import 'package:flutter/services.dart';
class ProximitySensor {
static const _channel = MethodChannel('com.example.proximity_sensor');
// Verifica se il sensore e disponibile
static Future<bool> isAvailable() async {
try {
final bool result = await _channel.invokeMethod('isAvailable');
return result;
} on PlatformException catch (e) {
// PlatformException: errore dal codice nativo
debugPrint('ProximitySensor.isAvailable error: ${e.code} - ${e.message}');
return false;
} on MissingPluginException {
// Plugin non registrato (es. desktop non supporta il sensore)
return false;
}
}
// Ottieni la distanza attuale (in cm, 0 = vicino, null = non disponibile)
static Future<double?> getDistance() async {
try {
final double? distance = await _channel.invokeMethod<double>('getDistance');
return distance;
} on PlatformException catch (e) {
throw ProximitySensorException(e.code, e.message ?? 'Unknown error');
}
}
}
class ProximitySensorException implements Exception {
final String code;
final String message;
const ProximitySensorException(this.code, this.message);
@override
String toString() => 'ProximitySensorException($code): $message';
}
// android/src/main/kotlin/.../ProximitySensorPlugin.kt
// Implementazione Android con Kotlin
package com.example.proximity_sensor
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorManager
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
class ProximitySensorPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
private lateinit var channel: MethodChannel
private lateinit var context: Context
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
channel = MethodChannel(
binding.binaryMessenger,
"com.example.proximity_sensor"
)
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
when (call.method) {
"isAvailable" -> {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
result.success(sensor != null)
}
"getDistance" -> {
val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager.getDefaultSensor(Sensor.TYPE_PROXIMITY)
if (sensor == null) {
// Errore: ritorna PlatformException a Dart
result.error(
"SENSOR_NOT_FOUND",
"Proximity sensor not available on this device",
null
)
return
}
// Leggi il valore attuale (semplificato: usa EventChannel per stream)
result.success(sensor.maximumRange.toDouble())
}
else -> result.notImplemented()
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
}
}
// ios/Classes/ProximitySensorPlugin.swift
// Implementazione iOS con Swift
import Flutter
import UIKit
public class ProximitySensorPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "com.example.proximity_sensor",
binaryMessenger: registrar.messenger()
)
let instance = ProximitySensorPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "isAvailable":
// iOS ha sempre il sensore di prossimita nei modelli standard
result(UIDevice.current.isProximityMonitoringEnabled ||
ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] == nil)
case "getDistance":
UIDevice.current.isProximityMonitoringEnabled = true
let isNear = UIDevice.current.proximityState
// Conversione in float: 0.0 = lontano, 1.0 = vicino
let distance: Double = isNear ? 0.0 : 10.0
result(distance)
default:
result(FlutterMethodNotImplemented)
}
}
}
EventChannel: Continuous Data Stream
L'EventChannel and designed for streaming data: sensors which update continuously, geolocation events, hardware notifications. The native emits events into a Dart stream that the widget can listen to with StreamBuilder.
// Dart: EventChannel per stream del sensore di prossimita
class ProximitySensorStream {
static const _eventChannel = EventChannel('com.example.proximity_sensor/stream');
static Stream<double> get proximityStream {
return _eventChannel
.receiveBroadcastStream()
.map((event) => (event as double));
}
}
// Widget: StreamBuilder per visualizzare i dati in tempo reale
class ProximitySensorWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return StreamBuilder<double>(
stream: ProximitySensorStream.proximityStream,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('Errore: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const CircularProgressIndicator();
}
final distance = snapshot.data!;
return Column(
children: [
Text('Distanza: ${distance.toStringAsFixed(1)} cm'),
Icon(
distance < 5 ? Icons.phone_in_talk : Icons.phone_android,
size: 48,
color: distance < 5 ? Colors.red : Colors.green,
),
],
);
},
);
}
}
// Kotlin: EventChannel sul lato Android
class ProximitySensorStreamHandler(
private val context: Context
) : EventChannel.StreamHandler {
private var sensorManager: SensorManager? = null
private var sensorListener: SensorEventListener? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
val sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_PROXIMITY)
if (sensor == null) {
events.error("SENSOR_NOT_FOUND", "No proximity sensor", null)
return
}
sensorListener = object : SensorEventListener {
override fun onSensorChanged(event: SensorEvent) {
// Invia ogni aggiornamento al Dart stream
events.success(event.values[0].toDouble())
}
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
}
sensorManager?.registerListener(sensorListener, sensor, SensorManager.SENSOR_DELAY_NORMAL)
}
override fun onCancel(arguments: Any?) {
// Pulisci le risorse quando lo stream viene cancellato
sensorManager?.unregisterListener(sensorListener)
sensorManager = null
sensorListener = null
}
}
Pigeon: Code Generation Type-Safe
Pigeon and the official Flutter tool to generate the code Platform Channels boilerplate from a shared Dart definition. Eliminates the need to manage manual serialization and guarantees that Dart and native code are in sync.
// pigeons/proximity.dart: definizione dell'interfaccia (source of truth)
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/proximity_api.g.dart',
kotlinOut: 'android/src/main/kotlin/com/example/ProximityApi.g.kt',
swiftOut: 'ios/Classes/ProximityApi.g.swift',
))
// Messaggi (data classes)
class ProximityInfo {
final double distance;
final bool isNear;
const ProximityInfo({required this.distance, required this.isNear});
}
// HostApi: metodi implementati nel codice nativo, chiamati da Dart
@HostApi()
abstract class ProximityHostApi {
bool isAvailable();
ProximityInfo getCurrentReading();
}
// FlutterApi: metodi implementati in Dart, chiamati dal nativo
@FlutterApi()
abstract class ProximityFlutterApi {
void onProximityChanged(ProximityInfo info);
}
// Genera il codice:
// flutter pub run pigeon --input pigeons/proximity.dart
When to Use Platform Channels vs Existing Plugins
- Before writing Platform Channels: search pub.dev to see if a plugin already exists
- pub.dev has over 40,000 packages: most of the native APIs are already covered
- Use Platform Channels for: Enterprise proprietary SDKs, very specific APIs, integration with existing native legacy code
- For new shareable plugins: use Pigeon for type safety and less maintenance
Conclusions
Platform Channels are the mechanism that makes Flutter truly universal: any native API and reachable with MethodChannel or EventChannel. Pigeon adds type safety and removes much of the boilerplate. The key to good native integration and properly handle errors with PlatformException and clean up resources (unregister listener, cancel stream) in the correct lifecycle.







