Perché servono i Platform Channels

Flutter offre un ricco set di plugin pronti all'uso, ma prima o poi capita di dover accedere a funzionalità native non ancora coperte: un SDK proprietario, un sensore particolare, API di sistema specifiche. In questi casi entrano in gioco i Platform Channels, il meccanismo ufficiale con cui il codice Dart comunica con il codice nativo (Kotlin/Java su Android, Swift/Objective-C su iOS).

La comunicazione è asincrona e basata sullo scambio di messaggi serializzati tramite un codec binario standard. Dart invia un messaggio, la piattaforma host lo riceve, esegue il lavoro nativo e restituisce un risultato.

I tre tipi di canale

  • MethodChannel: per invocare singoli metodi nativi e ricevere una risposta.
  • EventChannel: per ricevere flussi continui di dati dalla piattaforma (es. aggiornamenti di posizione, livello batteria).
  • BasicMessageChannel: per lo scambio di messaggi asincroni bidirezionali con un codec personalizzabile.

Esempio con MethodChannel

Supponiamo di voler recuperare il livello della batteria dal lato nativo. Sul lato Dart:

import 'package:flutter/services.dart';

class BatteryService {
  static const _channel = MethodChannel('it.esempio/battery');

  Future<int> getBatteryLevel() async {
    try {
      final level = await _channel.invokeMethod<int>('getBatteryLevel');
      return level ?? -1;
    } on PlatformException catch (e) {
      print('Errore nativo: ${e.message}');
      return -1;
    }
  }
}

Sul lato Android (Kotlin), in MainActivity.kt:

import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
import android.os.BatteryManager
import android.content.Context

class MainActivity : FlutterActivity() {
    private val CHANNEL = "it.esempio/battery"

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
            .setMethodCallHandler { call, result ->
                if (call.method == "getBatteryLevel") {
                    val bm = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
                    val level = bm.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
                    result.success(level)
                } else {
                    result.notImplemented()
                }
            }
    }
}

È fondamentale che il nome del canale sia identico su entrambi i lati e che venga gestito il caso notImplemented().

Streaming di dati con EventChannel

Quando i dati arrivano nel tempo, l'EventChannel espone uno Stream:

class SensorService {
  static const _events = EventChannel('it.esempio/sensor');

  Stream<double> get accelerometer =>
      _events.receiveBroadcastStream().map((e) => e as double);
}

Dal lato nativo si usa un EventChannel.StreamHandler che invia eventi tramite eventSink.success(...).

Pigeon: addio al boilerplate e ai typo

Scrivere manualmente la serializzazione dei dati e i nomi dei metodi è soggetto a errori. Pigeon è il pacchetto ufficiale di Google che genera codice type-safe per Dart, Kotlin/Java e Swift/Objective-C a partire da una definizione condivisa.

Si definisce un'interfaccia in un file Dart:

import 'package:pigeon/pigeon.dart';

class BatteryInfo {
  int? level;
  bool? isCharging;
}

@HostApi()
abstract class BatteryApi {
  BatteryInfo getBatteryInfo();
}

Poi si lancia il generatore:

dart run pigeon \
  --input pigeons/battery_api.dart \
  --dart_out lib/battery_api.g.dart \
  --kotlin_out android/app/src/main/kotlin/.../BatteryApi.g.kt \
  --swift_out ios/Runner/BatteryApi.g.swift

Pigeon crea automaticamente le classi e i metodi corrispondenti, eliminando le stringhe magiche e garantendo coerenza dei tipi a tempo di compilazione.

Best practice

  • Mantieni l'API minimale: passa solo i dati strettamente necessari attraverso il canale.
  • Gestisci sempre gli errori con PlatformException lato Dart e result.error(...) lato nativo.
  • Esegui il lavoro pesante su thread in background nel codice nativo, ma ricorda che la risposta al result deve avvenire sul main thread.
  • Preferisci Pigeon per progetti strutturati: riduce i bug e migliora la manutenibilità.
  • Isola la logica dietro un service Dart, così il resto dell'app non conosce i dettagli del canale.

Conclusione

I Platform Channels sono il ponte che rende Flutter davvero potente quando occorre uscire dal recinto delle API multipiattaforma. Con MethodChannel ed EventChannel copri la maggior parte dei casi, mentre Pigeon porta sicurezza dei tipi e meno boilerplate. Padroneggiare questi strumenti ti permette di integrare qualsiasi funzionalità nativa senza rinunciare alla produttività di Flutter.