[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-pigeon-in-flutter-platform-channel-type-safe-senza-boilerplate":3,"comments-article-pigeon-in-flutter-platform-channel-type-safe-senza-boilerplate":26},{"id":4,"title":5,"slug":6,"excerpt":7,"body":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"status":16,"published_at":17,"meta_title":18,"meta_description":19,"category":20,"author":24},122,"Pigeon in Flutter: platform channel type-safe senza boilerplate","pigeon-in-flutter-platform-channel-type-safe-senza-boilerplate","I MethodChannel scritti a mano sono fragili: stringhe magiche, cast dinamici e codice duplicato su tre linguaggi. Con Pigeon definisci l'interfaccia una volta in Dart e generi il codice per Dart, Kotlin e Swift. Guida pratica con esempi completi.","## Il problema dei MethodChannel scritti a mano\n\nChiunque abbia integrato codice nativo in Flutter conosce la trafila: si apre un `MethodChannel`, si sceglie un nome di canale, si inventano delle stringhe per i metodi e si scambiano `Map\u003CString, dynamic>` tra Dart e la piattaforma. Funziona, ma il compilatore non ti aiuta in nessun punto della catena:\n\n```dart\n\u002F\u002F Approccio classico: tutto è dynamic, tutto può rompersi a runtime\nfinal channel = MethodChannel('com.example\u002Fbattery');\nfinal result = await channel.invokeMethod('getBatteryInfo');\nfinal level = result['level'] as int; \u002F\u002F e se il nativo manda 'batteryLevel'?\n```\n\nBasta un refactor lato Kotlin, un typo in una chiave o un campo che diventa `null` e l'app crasha in produzione. **Pigeon**, il generatore di codice mantenuto dal team Flutter, risolve il problema alla radice: descrivi l'interfaccia una sola volta in un file Dart, lanci un comando e ottieni il codice di serializzazione per Dart, Kotlin, Swift (e opzionalmente C++, Objective-C, Java, GObject). Niente stringhe magiche, niente cast, errori di compilazione invece che crash a runtime.\n\n## Installazione e primo setup\n\nPigeon è un tool di sviluppo: non finisce nel bundle dell'app, quindi va in `dev_dependencies`.\n\n```yaml\ndev_dependencies:\n  pigeon: ^22.7.0\n```\n\nCrea una cartella `pigeons\u002F` nella root del progetto (per convenzione fuori da `lib\u002F`, così il file di definizione non viene compilato nell'app) e aggiungi `pigeons\u002Fmessages.dart`:\n\n```dart\nimport 'package:pigeon\u002Fpigeon.dart';\n\n@ConfigurePigeon(PigeonOptions(\n  dartOut: 'lib\u002Fsrc\u002Fplatform\u002Fmessages.g.dart',\n  dartTestOut: 'test\u002Fplatform\u002Ftest_messages.g.dart',\n  kotlinOut:\n      'android\u002Fapp\u002Fsrc\u002Fmain\u002Fkotlin\u002Fcom\u002Fexample\u002Fmyapp\u002FMessages.g.kt',\n  kotlinOptions: KotlinOptions(package: 'com.example.myapp'),\n  swiftOut: 'ios\u002FRunner\u002FMessages.g.swift',\n  dartPackageName: 'myapp',\n))\nlibrary;\n```\n\nIl blocco `@ConfigurePigeon` evita di passare decine di flag da riga di comando: la configurazione vive nel repository ed è riproducibile da chiunque.\n\n## Definire i modelli e le API\n\nNello stesso file descrivi i tipi di dato e le interfacce. Le classi di Pigeon sono **solo contenitori di campi**: niente costruttori custom, niente metodi, niente logica.\n\n```dart\nenum ChargingState { charging, discharging, full, unknown }\n\nclass BatteryInfo {\n  BatteryInfo({\n    required this.level,\n    required this.state,\n    this.temperatureCelsius,\n  });\n\n  int level;\n  ChargingState state;\n  double? temperatureCelsius;\n}\n\nclass DeviceQuery {\n  DeviceQuery({required this.includeTemperature, this.tags});\n\n  bool includeTemperature;\n  List\u003CString>? tags;\n}\n```\n\nOra le due interfacce fondamentali:\n\n```dart\n\u002F\u002F\u002F Metodi implementati dalla piattaforma e chiamati da Dart.\n@HostApi()\nabstract class BatteryHostApi {\n  BatteryInfo getBatteryInfo(DeviceQuery query);\n\n  \u002F\u002F\u002F `@async` genera una callback lato nativo: utile per operazioni\n  \u002F\u002F\u002F che non possono rispondere in modo sincrono.\n  @async\n  bool requestIgnoreBatteryOptimizations();\n\n  void setLowPowerThreshold(int percentage);\n}\n\n\u002F\u002F\u002F Metodi implementati in Dart e chiamati dalla piattaforma.\n@FlutterApi()\nabstract class BatteryFlutterApi {\n  void onChargingStateChanged(ChargingState state);\n  void onCriticalLevel(int level);\n}\n```\n\nLa distinzione è il cuore di Pigeon:\n\n- `@HostApi` → Flutter chiama il nativo (Dart ottiene una classe concreta, il nativo un'interfaccia da implementare).\n- `@FlutterApi` → il nativo chiama Flutter (Dart ottiene una classe astratta da implementare, il nativo una classe concreta da invocare).\n\n## Generare il codice\n\n```bash\ndart run pigeon --input pigeons\u002Fmessages.dart\n```\n\nIl comando produce i file indicati in `PigeonOptions`. **I file generati vanno committati**: il codice nativo li referenzia direttamente e la build Gradle\u002FXcode non esegue Pigeon. Conviene aggiungere uno script al progetto:\n\n```bash\n# tool\u002Fgenerate_pigeons.sh\n#!\u002Fusr\u002Fbin\u002Fenv bash\nset -e\nfor file in pigeons\u002F*.dart; do\n  dart run pigeon --input \"$file\"\ndone\ndart format lib\u002Fsrc\u002Fplatform test\u002Fplatform\n```\n\nSe usi analysis_options severo, escludi i generati:\n\n```yaml\nanalyzer:\n  exclude:\n    - \"**\u002F*.g.dart\"\n```\n\n## Implementazione su Android (Kotlin)\n\nPigeon genera l'interfaccia `BatteryHostApi` e la funzione statica `setUp`. Si implementa così:\n\n```kotlin\nimport android.content.Context\nimport android.os.BatteryManager\n\nclass BatteryHostApiImpl(private val context: Context) : BatteryHostApi {\n\n  override fun getBatteryInfo(query: DeviceQuery): BatteryInfo {\n    val manager = context.getSystemService(Context.BATTERY_SERVICE)\n        as BatteryManager\n    val level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)\n\n    if (level \u003C 0) {\n      \u002F\u002F Diventa una PlatformException lato Dart\n      throw FlutterError(\n        code = \"battery_unavailable\",\n        message = \"Livello batteria non disponibile\",\n        details = null,\n      )\n    }\n\n    return BatteryInfo(\n      level = level.toLong(),\n      state = ChargingState.CHARGING,\n      temperatureCelsius = if (query.includeTemperature) 31.5 else null,\n    )\n  }\n\n  override fun requestIgnoreBatteryOptimizations(\n    callback: (Result\u003CBoolean>) -> Unit\n  ) {\n    \u002F\u002F operazione asincrona: la callback può essere invocata più tardi\n    callback(Result.success(true))\n  }\n\n  override fun setLowPowerThreshold(percentage: Long) {\n    \u002F\u002F ...\n  }\n}\n```\n\nNota un dettaglio importante: gli `int` di Dart diventano `Long` in Kotlin e `Int64` in Swift, perché il protocollo di trasporto usa interi a 64 bit.\n\nLa registrazione va fatta dove hai accesso al `BinaryMessenger`:\n\n```kotlin\nclass MainActivity : FlutterActivity() {\n  private var flutterApi: BatteryFlutterApi? = null\n\n  override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n    super.configureFlutterEngine(flutterEngine)\n    val messenger = flutterEngine.dartExecutor.binaryMessenger\n\n    BatteryHostApi.setUp(messenger, BatteryHostApiImpl(this))\n    flutterApi = BatteryFlutterApi(messenger)\n  }\n\n  private fun notifyCharging(state: ChargingState) {\n    \u002F\u002F chiamata dal nativo verso Dart\n    flutterApi?.onChargingStateChanged(state) { }\n  }\n}\n```\n\n## Implementazione su iOS (Swift)\n\n```swift\nimport Flutter\nimport UIKit\n\nclass BatteryHostApiImpl: BatteryHostApi {\n\n  func getBatteryInfo(query: DeviceQuery) throws -> BatteryInfo {\n    UIDevice.current.isBatteryMonitoringEnabled = true\n    let level = UIDevice.current.batteryLevel\n\n    guard level >= 0 else {\n      throw PigeonError(\n        code: \"battery_unavailable\",\n        message: \"Livello batteria non disponibile\",\n        details: nil\n      )\n    }\n\n    return BatteryInfo(\n      level: Int64(level * 100),\n      state: UIDevice.current.batteryState == .charging\n        ? .charging : .discharging,\n      temperatureCelsius: query.includeTemperature ? 31.5 : nil\n    )\n  }\n\n  func requestIgnoreBatteryOptimizations(\n    completion: @escaping (Result\u003CBool, Error>) -> Void\n  ) {\n    completion(.success(false)) \u002F\u002F non esiste su iOS\n  }\n\n  func setLowPowerThreshold(percentage: Int64) throws {}\n}\n```\n\nRegistrazione nell'`AppDelegate`:\n\n```swift\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n  var flutterApi: BatteryFlutterApi?\n\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions:\n      [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    let controller = window?.rootViewController as! FlutterViewController\n    BatteryHostApiSetup.setUp(\n      binaryMessenger: controller.binaryMessenger,\n      api: BatteryHostApiImpl()\n    )\n    flutterApi = BatteryFlutterApi(binaryMessenger: controller.binaryMessenger)\n\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n}\n```\n\n## Usare l'API dal lato Dart\n\nIl codice generato è completamente tipizzato: niente `dynamic`, niente chiavi stringa.\n\n```dart\nimport 'package:myapp\u002Fsrc\u002Fplatform\u002Fmessages.g.dart';\n\nclass BatteryRepository implements BatteryFlutterApi {\n  BatteryRepository() {\n    \u002F\u002F registra l'implementazione Dart chiamata dal nativo\n    BatteryFlutterApi.setUp(this);\n  }\n\n  final _host = BatteryHostApi();\n  final _stateController = StreamController\u003CChargingState>.broadcast();\n\n  Stream\u003CChargingState> get chargingState => _stateController.stream;\n\n  Future\u003CBatteryInfo> load() async {\n    try {\n      return await _host.getBatteryInfo(\n        DeviceQuery(includeTemperature: true, tags: const ['ui']),\n      );\n    } on PlatformException catch (e) {\n      \u002F\u002F il FlutterError\u002FPigeonError nativo arriva qui\n      throw BatteryUnavailableException(e.message);\n    }\n  }\n\n  @override\n  void onChargingStateChanged(ChargingState state) =>\n      _stateController.add(state);\n\n  @override\n  void onCriticalLevel(int level) {\n    \u002F\u002F mostra un avviso\n  }\n}\n```\n\nSe domani aggiungi un campo a `BatteryInfo` e rigeneri, Kotlin e Swift **non compilano** finché non lo gestisci. È esattamente la rete di sicurezza che manca ai MethodChannel manuali.\n\n## Stream nativi con @EventChannelApi\n\nPer flussi continui di eventi (posizione, sensori, stato batteria) le `@FlutterApi` funzionano, ma dalle versioni recenti Pigeon offre un'annotazione dedicata che genera un vero `EventChannel` tipizzato:\n\n```dart\n@EventChannelApi()\nabstract class BatteryEventApi {\n  ChargingState streamChargingState();\n  int streamLevel();\n}\n```\n\nLato Dart ottieni funzioni che restituiscono `Stream\u003CChargingState>` e `Stream\u003Cint>`; lato nativo devi fornire uno `StreamHandler` generato:\n\n```dart\nfinal sub = streamChargingState().listen((state) {\n  debugPrint('Nuovo stato: $state');\n});\n```\n\n```kotlin\nclass ChargingStateHandler : StreamChargingStateStreamHandler() {\n  override fun onListen(p0: Any?, sink: PigeonEventSink\u003CChargingState>) {\n    sink.success(ChargingState.CHARGING)\n    \u002F\u002F sink.endOfStream() quando il flusso termina\n  }\n}\n\n\u002F\u002F registrazione\nBatteryEventApi.register(messenger, ChargingStateHandler(), \"streamChargingState\")\n```\n\nÈ una feature relativamente nuova: verifica il changelog della versione che usi, perché le firme generate sono cambiate tra una release e l'altra.\n\n## Testare senza dispositivo\n\nL'opzione `dartTestOut` genera, per ogni `@HostApi`, una classe `TestBatteryHostApi` che puoi estendere nei test: intercetta i messaggi binari e risponde senza bisogno di codice nativo.\n\n```dart\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:myapp\u002Fsrc\u002Fplatform\u002Fmessages.g.dart';\nimport 'package:myapp\u002Ftest\u002Fplatform\u002Ftest_messages.g.dart';\n\nclass FakeBatteryHost extends TestBatteryHostApi {\n  @override\n  BatteryInfo getBatteryInfo(DeviceQuery query) => BatteryInfo(\n        level: 42,\n        state: ChargingState.discharging,\n        temperatureCelsius: query.includeTemperature ? 30 : null,\n      );\n\n  @override\n  Future\u003Cbool> requestIgnoreBatteryOptimizations() async => true;\n\n  @override\n  void setLowPowerThreshold(int percentage) {}\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  setUp(() => TestBatteryHostApi.setUp(FakeBatteryHost()));\n\n  test('legge il livello dalla piattaforma', () async {\n    final info = await BatteryRepository().load();\n    expect(info.level, 42);\n    expect(info.state, ChargingState.discharging);\n  });\n}\n```\n\nCosì la logica Dart che dipende dalla piattaforma è coperta da unit test veloci, senza emulatori.\n\n## Dettagli che fanno la differenza\n\n**Più istanze dello stesso canale.** Se la tua API deve servire più oggetti nativi (per esempio una API per ogni connessione), usa `messageChannelSuffix`:\n\n```dart\n@HostApi(dartHostTestHandler: 'TestBatteryHostApi')\nabstract class BatteryHostApi { \u002F* ... *\u002F }\n```\n\ne lato Dart `BatteryHostApi(messageChannelSuffix: sessionId)`, passando lo stesso suffisso al `setUp` nativo.\n\n**Tipi supportati.** `bool`, `int`, `double`, `String`, `Uint8List`, `Int32List`, `Float64List`, `List`, `Map`, `Object`, enum e le classi Pigeon. Nessun `DateTime`, nessun `Duration`: serializzali come `int` (millisecondi) o `String` ISO-8601.\n\n**Nullability.** Pigeon rispetta la null safety su tutti i linguaggi: un campo `String?` diventa opzionale anche in Kotlin e Swift. È uno dei vantaggi più sottovalutati rispetto alle mappe manuali.\n\n**Thread.** Le `@HostApi` sincrone vengono eseguite sul thread della piattaforma (UI thread su Android). Se l'operazione è costosa usa `@async` e sposta il lavoro su un executor o su una `DispatchQueue` in background, invocando la callback al termine.\n\n**Plugin federati.** Se stai scrivendo un package, Pigeon è perfetto per l'implementazione di piattaforma: imposta `dartOut` dentro `lib\u002Fsrc\u002F` del package e tieni le classi generate private, esponendo una API pubblica scritta a mano.\n\n## Quando non serve\n\nPigeon risolve la comunicazione *a messaggi*. Non sostituisce:\n\n- **Dart FFI**, più adatto a chiamate sincrone verso librerie C\u002FC++ ad alta frequenza;\n- le **Platform View**, quando devi incorporare una view nativa nell'albero dei widget;\n- i casi banali con una sola chiamata senza parametri, dove un `MethodChannel` di cinque righe è ancora accettabile (anche se la coerenza del progetto spesso vale più della brevità).\n\n## Conclusione\n\nPigeon trasforma il punto più fragile di un'app Flutter — il confine con il codice nativo — in un contratto verificato dal compilatore su tre linguaggi. Il costo di adozione è una cartella `pigeons\u002F`, uno script di generazione e l'abitudine a rigenerare dopo ogni modifica; il ritorno è l'eliminazione di un'intera categoria di bug runtime, più test unitari che girano senza dispositivo. Se hai un'app con integrazioni native non banali, migrare un canale alla volta è un investimento che si ripaga al primo refactor.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fc344c763-7a4c-4840-a05c-8cf07103289b.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1617696992381-16b65f34b3b1?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTE1MTg0OTd8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Saif71.com","https:\u002F\u002Funsplash.com\u002F@saif71","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fsilver-iphone-6-with-black-case--G28lojV0gk",null,"published","2026-10-09T04:01:37+00:00","Pigeon in Flutter: platform channel type-safe","Guida pratica a Pigeon: genera codice type-safe per Dart, Kotlin e Swift, elimina i MethodChannel scritti a mano e testa le API native senza dispositivo.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316906676]