[{"data":1,"prerenderedAt":87},["ShallowReactive",2],{"tutorial-platform-channel-in-flutter-chiamare-codice-nativo-android-e-ios":3,"comments-tutorial-platform-channel-in-flutter-chiamare-codice-nativo-android-e-ios":86},{"id":4,"title":5,"slug":6,"excerpt":7,"intro":8,"cover_image":9,"cover_remote_url":10,"cover_credit":11,"video_url":15,"video":16,"difficulty":20,"estimated_minutes":21,"flutter_version":22,"status":23,"published_at":24,"meta_title":25,"meta_description":26,"category":27,"author":31,"steps":33},87,"Platform Channel in Flutter: chiamare codice nativo Android e iOS","platform-channel-in-flutter-chiamare-codice-nativo-android-e-ios","Guida avanzata ai platform channel: MethodChannel, EventChannel, gestione degli errori, thread nativi, test con mock e migrazione a Pigeon per una API type-safe tra Dart, Kotlin e Swift.","Prima o poi ogni progetto Flutter incontra un muro: una funzionalità che il framework non espone e per cui non esiste un plugin affidabile su pub.dev. Livello della batteria, impostazioni di sistema, un SDK di terze parti distribuito solo come libreria nativa, un sensore proprietario.\n\nLa risposta di Flutter sono i **platform channel**: un ponte asincrono e bidirezionale tra il codice Dart e il codice nativo (Kotlin\u002FJava su Android, Swift\u002FObjective-C su iOS). I messaggi viaggiano serializzati sul *binary messenger* dell'engine, quindi ogni chiamata è asincrona per definizione.\n\nIn questo tutorial costruiamo un piccolo bridge `DeviceBridge` che:\n\n- legge il **livello della batteria** tramite `MethodChannel`;\n- riceve gli **aggiornamenti di stato di carica in streaming** tramite `EventChannel`;\n- gestisce correttamente `PlatformException`, `MissingPluginException` e il lavoro su thread secondari;\n- è **testabile** senza dispositivo grazie al mock del binary messenger;\n- può evolvere verso **Pigeon** per generare codice type-safe ed eliminare le stringhe magiche.\n\n> Prerequisiti: Flutter 3.x, Android Studio con toolchain Android e (per la parte iOS) Xcode su macOS. Serve dimestichezza con Kotlin e Swift di base.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002F6e0609ed-6d0d-4139-98c4-ec14596caaf2.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1739672041198-2c4c84200dfc?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODg3NTU1MTh8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Silvio Wiggelinghoff","https:\u002F\u002Funsplash.com\u002F@silviovisuals","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-large-bridge-over-a-large-body-of-water-qPzYJvopYKk","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=MbBgcOtWjF4",{"youtube_id":17,"duration_seconds":18,"published_at":19},"MbBgcOtWjF4",147,"2026-09-07T09:51:51+00:00","advanced",55,"3.x","published","2026-09-07T04:31:58+00:00","Platform Channel Flutter: codice nativo Android e iOS","Tutorial avanzato sui platform channel Flutter: MethodChannel, EventChannel, Kotlin e Swift, gestione errori, thread, test con mock e Pigeon.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,58,65,72,79],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},578,"Progettare il contratto e creare il wrapper Dart","Il primo errore da evitare è spargere `MethodChannel` in giro per l'app. Il canale è un **dettaglio infrastrutturale**: va incapsulato in una classe di dominio che espone metodi Dart tipizzati e traduce gli errori nativi in eccezioni applicative.\n\nRegole per il contratto:\n\n1. **Nome del canale univoco e con namespace**: usa il tuo dominio inverso, ad esempio `dev.miosito.app\u002Fdevice`. Due canali con lo stesso nome (magari introdotti da un plugin) si sovrascrivono a vicenda.\n2. **Payload solo con tipi supportati** dal `StandardMessageCodec`: `null`, `bool`, `int`, `double`, `String`, `Uint8List`, `Int32List`, `Int64List`, `Float64List`, `List` e `Map`. Niente oggetti custom: serializzali in `Map\u003CString, Object?>`.\n3. **Argomenti sempre come mappa**, anche se oggi ne passi uno solo: aggiungere un parametro domani non romperà il codice nativo.\n\nCreiamo `lib\u002Fplatform\u002Fdevice_bridge.dart` con il canale, un metodo `getBatteryLevel()` e una eccezione di dominio.","import 'package:flutter\u002Fservices.dart';\n\n\u002F\u002F\u002F Errore di dominio: nasconde al resto dell'app i dettagli del canale.\nclass DeviceBridgeException implements Exception {\n  const DeviceBridgeException(this.code, this.message);\n\n  final String code;\n  final String message;\n\n  @override\n  String toString() => 'DeviceBridgeException($code): $message';\n}\n\nclass DeviceBridge {\n  DeviceBridge({MethodChannel? channel})\n      : _channel = channel ?? const MethodChannel(_channelName);\n\n  static const String _channelName = 'dev.miosito.app\u002Fdevice';\n\n  final MethodChannel _channel;\n\n  \u002F\u002F\u002F Ritorna il livello batteria in percentuale (0-100).\n  Future\u003Cint> getBatteryLevel() async {\n    try {\n      final int? level = await _channel.invokeMethod\u003Cint>('getBatteryLevel');\n      if (level == null) {\n        throw const DeviceBridgeException(\n          'NULL_RESULT',\n          'Il lato nativo ha restituito null.',\n        );\n      }\n      return level;\n    } on PlatformException catch (e) {\n      throw DeviceBridgeException(e.code, e.message ?? 'Errore nativo');\n    } on MissingPluginException {\n      \u002F\u002F Succede su piattaforme dove non abbiamo implementato il canale\n      \u002F\u002F (web, desktop) oppure dopo un hot restart senza rebuild nativo.\n      throw const DeviceBridgeException(\n        'UNSUPPORTED_PLATFORM',\n        'Funzionalità non disponibile su questa piattaforma.',\n      );\n    }\n  }\n\n  \u002F\u002F\u002F Esempio di chiamata con argomenti tipizzati.\n  Future\u003Cvoid> setKeepScreenOn({required bool enabled}) async {\n    await _channel.invokeMethod\u003Cvoid>('setKeepScreenOn', \u003CString, Object?>{\n      'enabled': enabled,\n    });\n  }\n}","dart","Il progetto compila e il resto dell'app dipende solo da `DeviceBridge`, mai da `MethodChannel`. Le chiamate lanciano ancora `UNSUPPORTED_PLATFORM` perché il lato nativo non esiste.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},579,2,"Implementare il lato Android in Kotlin","Apri la cartella `android\u002F` come progetto in Android Studio (così hai autocompletamento e analisi Kotlin) e modifica `MainActivity.kt`.\n\nPunti chiave:\n\n- Registra l'handler dentro `configureFlutterEngine`, **non** in `onCreate`: è il momento in cui il `binaryMessenger` è disponibile.\n- L'handler viene invocato sul **main thread** di Android. Se l'operazione è lenta (I\u002FO, rete, crittografia) spostala su un thread secondario e **rispondi sempre sul main thread**, altrimenti l'app può crashare in modo non deterministico.\n- Usa `result.error(code, message, details)` per gli errori e `result.notImplemented()` per i metodi sconosciuti: quest'ultimo produce una `MissingPluginException` lato Dart, utile per capire subito un disallineamento di contratto.\n- Ricorda `flutterEngine.dartExecutor.binaryMessenger` come messenger.\n\nSe il tuo canale serve più feature, estrai una classe dedicata (es. `DevicePlugin`) invece di gonfiare `MainActivity`.","package dev.miosito.app\n\nimport android.content.Context\nimport android.content.ContextWrapper\nimport android.os.BatteryManager\nimport android.os.Build\nimport android.view.WindowManager\nimport io.flutter.embedding.android.FlutterActivity\nimport io.flutter.embedding.engine.FlutterEngine\nimport io.flutter.plugin.common.MethodCall\nimport io.flutter.plugin.common.MethodChannel\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.launch\nimport kotlinx.coroutines.withContext\n\nclass MainActivity : FlutterActivity() {\n\n    private val scope = CoroutineScope(Dispatchers.Main)\n\n    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {\n        super.configureFlutterEngine(flutterEngine)\n\n        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)\n            .setMethodCallHandler { call, result -> handle(call, result) }\n    }\n\n    private fun handle(call: MethodCall, result: MethodChannel.Result) {\n        when (call.method) {\n            \"getBatteryLevel\" -> scope.launch {\n                \u002F\u002F Lavoro potenzialmente lento fuori dal main thread...\n                val level = withContext(Dispatchers.Default) { readBatteryLevel() }\n                \u002F\u002F ...ma risposta SEMPRE sul main thread.\n                if (level >= 0) {\n                    result.success(level)\n                } else {\n                    result.error(\n                        \"BATTERY_UNAVAILABLE\",\n                        \"Impossibile leggere il livello della batteria\",\n                        null,\n                    )\n                }\n            }\n\n            \"setKeepScreenOn\" -> {\n                val enabled = call.argument\u003CBoolean>(\"enabled\")\n                if (enabled == null) {\n                    result.error(\"BAD_ARGS\", \"Parametro 'enabled' mancante\", null)\n                    return\n                }\n                if (enabled) {\n                    window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n                } else {\n                    window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)\n                }\n                result.success(null)\n            }\n\n            else -> result.notImplemented()\n        }\n    }\n\n    private fun readBatteryLevel(): Int {\n        val manager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager\n        return manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)\n    }\n\n    companion object {\n        private const val CHANNEL = \"dev.miosito.app\u002Fdevice\"\n    }\n}","kotlin","Su un dispositivo o emulatore Android, `DeviceBridge().getBatteryLevel()` restituisce un intero tra 0 e 100. Nota: dopo aver toccato codice nativo serve un **full restart** (`flutter run`), l'hot reload non ricompila Kotlin.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":56,"expected_result":57,"demo_url":41,"video_url":41},580,3,"Implementare il lato iOS in Swift","Apri `ios\u002FRunner.xcworkspace` con Xcode e modifica `AppDelegate.swift`.\n\nDifferenze rispetto ad Android:\n\n- Il canale si aggancia al `FlutterViewController` ottenuto da `window?.rootViewController`.\n- Il codice degli handler gira sul **main thread** (UI thread); vale la stessa regola: lavoro pesante su una `DispatchQueue` in background, `result(...)` di ritorno su `DispatchQueue.main`.\n- Per gli errori si usa `FlutterError(code:message:details:)`; per i metodi sconosciuti `FlutterMethodNotImplemented`.\n- I tipi Dart mappano su tipi Foundation: `int` → `NSNumber`, `Map` → `[String: Any]`. Fai sempre il cast difensivo degli argomenti.\n\nNel nostro esempio il livello batteria richiede di abilitare `isBatteryMonitoringEnabled`; su simulatore può restituire `-1`, motivo per cui gestiamo esplicitamente il caso di errore.","import UIKit\nimport Flutter\n\n@main\n@objc class AppDelegate: FlutterAppDelegate {\n\n  private let channelName = \"dev.miosito.app\u002Fdevice\"\n\n  override func application(\n    _ application: UIApplication,\n    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?\n  ) -> Bool {\n    let controller = window?.rootViewController as! FlutterViewController\n    let channel = FlutterMethodChannel(\n      name: channelName,\n      binaryMessenger: controller.binaryMessenger\n    )\n\n    channel.setMethodCallHandler { [weak self] call, result in\n      guard let self = self else { return }\n      switch call.method {\n      case \"getBatteryLevel\":\n        self.handleBatteryLevel(result: result)\n\n      case \"setKeepScreenOn\":\n        guard let args = call.arguments as? [String: Any],\n              let enabled = args[\"enabled\"] as? Bool else {\n          result(FlutterError(code: \"BAD_ARGS\",\n                              message: \"Parametro 'enabled' mancante\",\n                              details: nil))\n          return\n        }\n        UIApplication.shared.isIdleTimerDisabled = enabled\n        result(nil)\n\n      default:\n        result(FlutterMethodNotImplemented)\n      }\n    }\n\n    GeneratedPluginRegistrant.register(with: self)\n    return super.application(application, didFinishLaunchingWithOptions: launchOptions)\n  }\n\n  private func handleBatteryLevel(result: @escaping FlutterResult) {\n    let device = UIDevice.current\n    device.isBatteryMonitoringEnabled = true\n    let level = device.batteryLevel\n    if level \u003C 0 {\n      result(FlutterError(code: \"BATTERY_UNAVAILABLE\",\n                          message: \"Livello batteria non disponibile (simulatore?)\",\n                          details: nil))\n    } else {\n      result(Int(level * 100))\n    }\n  }\n}","swift","Su dispositivo iOS reale la percentuale viene restituita correttamente; su simulatore ottieni un `DeviceBridgeException('BATTERY_UNAVAILABLE', ...)` gestito in modo pulito dall'app.",{"id":59,"position":60,"title":61,"body":62,"code_snippet":63,"code_language":39,"expected_result":64,"demo_url":41,"video_url":41},581,4,"Streaming di eventi nativi con EventChannel","`MethodChannel` è request\u002Fresponse. Quando il nativo deve **spingere** dati verso Dart in modo continuo (sensori, stato batteria, connessioni BLE, progressi di un download nativo) si usa `EventChannel`, che espone lato Dart uno `Stream`.\n\nIl ciclo di vita è importante: `onListen` viene chiamato quando Dart si iscrive, `onCancel` quando annulla la sottoscrizione. **Registra le risorse native in `onListen` e liberale in `onCancel`**, altrimenti i broadcast receiver o gli osservatori restano attivi e consumano batteria.\n\nAggiungiamo al wrapper Dart lo stream e implementiamolo su Android con un `BroadcastReceiver`.","\u002F\u002F --- Dart: aggiunta a DeviceBridge -----------------------------------\nstatic const EventChannel _chargingChannel =\n    EventChannel('dev.miosito.app\u002Fdevice\u002Fcharging');\n\n\u002F\u002F\u002F Emette true quando il dispositivo è in carica.\nStream\u003Cbool> get chargingStatus => _chargingChannel\n    .receiveBroadcastStream()\n    .map((dynamic event) => event as bool)\n    .handleError((Object error) {\n      final e = error as PlatformException;\n      throw DeviceBridgeException(e.code, e.message ?? 'Errore stream');\n    });\n\n\u002F\u002F Uso nella UI:\n\u002F\u002F StreamBuilder\u003Cbool>(\n\u002F\u002F   stream: bridge.chargingStatus,\n\u002F\u002F   builder: (context, snapshot) => Text(snapshot.data == true ? 'In carica' : 'A batteria'),\n\u002F\u002F );","Il wrapper Dart espone `chargingStatus`. Manca ancora l'implementazione nativa dell'EventChannel.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":48,"expected_result":71,"demo_url":41,"video_url":41},582,5,"Implementare lo StreamHandler nativo (Android)","Sul lato Android l'`EventChannel` richiede un `EventChannel.StreamHandler`. Nel nostro caso registriamo un `BroadcastReceiver` sull'action `ACTION_POWER_CONNECTED`\u002F`ACTION_POWER_DISCONNECTED` e inoltriamo l'evento con `events.success(...)`.\n\nAttenzione a due dettagli spesso trascurati:\n\n1. `EventSink` **non è thread-safe**: se generi eventi da un thread di background, inoltrali al main thread con un `Handler(Looper.getMainLooper())`.\n2. Deregistra il receiver in `onCancel` **e** gestisci il caso di doppia cancellazione (imposta il riferimento a `null`).\n\nLo stesso pattern su iOS si ottiene implementando il protocollo `FlutterStreamHandler` con i metodi `onListen(withArguments:eventSink:)` e `onCancel(withArguments:)`, tipicamente osservando `UIDevice.batteryStateDidChangeNotification`.","package dev.miosito.app\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.os.Handler\nimport android.os.Looper\nimport io.flutter.plugin.common.EventChannel\n\nclass ChargingStreamHandler(\n    private val context: Context,\n) : EventChannel.StreamHandler {\n\n    private var receiver: BroadcastReceiver? = null\n    private val mainHandler = Handler(Looper.getMainLooper())\n\n    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\n        if (events == null) return\n\n        val broadcastReceiver = object : BroadcastReceiver() {\n            override fun onReceive(ctx: Context?, intent: Intent?) {\n                val charging = intent?.action == Intent.ACTION_POWER_CONNECTED\n                \u002F\u002F EventSink va usato sul main thread.\n                mainHandler.post { events.success(charging) }\n            }\n        }\n\n        val filter = IntentFilter().apply {\n            addAction(Intent.ACTION_POWER_CONNECTED)\n            addAction(Intent.ACTION_POWER_DISCONNECTED)\n        }\n        context.registerReceiver(broadcastReceiver, filter)\n        receiver = broadcastReceiver\n    }\n\n    override fun onCancel(arguments: Any?) {\n        receiver?.let { context.unregisterReceiver(it) }\n        receiver = null\n    }\n}\n\n\u002F\u002F In MainActivity.configureFlutterEngine:\n\u002F\u002F EventChannel(flutterEngine.dartExecutor.binaryMessenger, \"dev.miosito.app\u002Fdevice\u002Fcharging\")\n\u002F\u002F     .setStreamHandler(ChargingStreamHandler(applicationContext))","Collegando e scollegando il cavo (o cambiando lo stato di carica dell'emulatore) lo `StreamBuilder` aggiorna la UI in tempo reale. Uscendo dalla schermata il receiver viene deregistrato senza leak.",{"id":73,"position":74,"title":75,"body":76,"code_snippet":77,"code_language":39,"expected_result":78,"demo_url":41,"video_url":41},583,6,"Testare i platform channel senza dispositivo","Il codice nativo non è raggiungibile dai test Dart, ma il **binary messenger è sostituibile**: possiamo intercettare le chiamate del canale e restituire risposte finte. È il modo corretto per testare il wrapper, la mappatura degli errori e la logica di dominio che ne dipende.\n\nDa Flutter 3.x l'API raccomandata è `TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(...)` (le vecchie `MethodChannel.setMockMethodCallHandler` sono deprecate).\n\nRicorda sempre di **azzerare l'handler** in `tearDown`, altrimenti i mock si propagano tra i test.","import 'package:flutter\u002Fservices.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:mia_app\u002Fplatform\u002Fdevice_bridge.dart';\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  const channel = MethodChannel('dev.miosito.app\u002Fdevice');\n  final messenger =\n      TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;\n  final bridge = DeviceBridge();\n\n  tearDown(() {\n    messenger.setMockMethodCallHandler(channel, null);\n  });\n\n  test('getBatteryLevel restituisce il valore nativo', () async {\n    final calls = \u003CMethodCall>[];\n    messenger.setMockMethodCallHandler(channel, (call) async {\n      calls.add(call);\n      return 73;\n    });\n\n    expect(await bridge.getBatteryLevel(), 73);\n    expect(calls.single.method, 'getBatteryLevel');\n  });\n\n  test('PlatformException viene tradotta in DeviceBridgeException', () async {\n    messenger.setMockMethodCallHandler(channel, (call) async {\n      throw PlatformException(\n        code: 'BATTERY_UNAVAILABLE',\n        message: 'non disponibile',\n      );\n    });\n\n    expect(\n      () => bridge.getBatteryLevel(),\n      throwsA(isA\u003CDeviceBridgeException>()\n          .having((e) => e.code, 'code', 'BATTERY_UNAVAILABLE')),\n    );\n  });\n\n  test('metodo non implementato => UNSUPPORTED_PLATFORM', () async {\n    \u002F\u002F Nessun handler registrato: il canale lancia MissingPluginException.\n    expect(\n      () => bridge.getBatteryLevel(),\n      throwsA(isA\u003CDeviceBridgeException>()\n          .having((e) => e.code, 'code', 'UNSUPPORTED_PLATFORM')),\n    );\n  });\n}","`flutter test` passa i tre test in pochi secondi, su qualsiasi macchina, senza emulatori: il contratto del canale è ora protetto da regressioni.",{"id":80,"position":81,"title":82,"body":83,"code_snippet":84,"code_language":39,"expected_result":85,"demo_url":41,"video_url":41},584,7,"Eliminare le stringhe magiche con Pigeon","I `MethodChannel` scritti a mano hanno tre punti deboli: nomi dei metodi come stringhe, argomenti non tipizzati e nessun controllo a compile-time tra Dart e nativo. **Pigeon** risolve tutto generando l'interfaccia Dart, Kotlin e Swift a partire da un unico file di definizione.\n\nPassi operativi:\n\n1. `dev_dependencies: pigeon: ^22.0.0` in `pubspec.yaml`.\n2. Crea `pigeons\u002Fdevice_api.dart` con la definizione (vedi codice).\n3. Esegui la generazione:\n   `dart run pigeon --input pigeons\u002Fdevice_api.dart`\n4. Implementa in Kotlin `DeviceApi` (interfaccia generata) e registrala con `DeviceApi.setUp(binaryMessenger, MyDeviceApi())`; in Swift `DeviceApiSetup.setUp(binaryMessenger:api:)`.\n\nDa quel momento un rename di un metodo rompe la build nativa invece di fallire a runtime. Nota: Pigeon copre chiamate request\u002Fresponse (in entrambe le direzioni con `@FlutterApi`), mentre per lo streaming continuo resta valido `EventChannel` (o gli event channel generati nelle versioni recenti di Pigeon).\n\n**Checklist finale di produzione**\n\n- Ogni metodo nativo risponde **esattamente una volta** (`result.success`\u002F`result.error`): rispondere due volte fa crashare l'engine.\n- Non bloccare mai il main thread nativo.\n- Se il canale serve fuori dalla UI (background isolate, `FlutterEngineGroup`), usa un `FlutterEngine` dedicato con il proprio messenger.\n- Se la funzionalità è riusabile, impacchettala in un **federated plugin** (`flutter create --template=plugin`) invece di lasciarla in `MainActivity`\u002F`AppDelegate`.","\u002F\u002F pigeons\u002Fdevice_api.dart (NON viene compilato nell'app)\nimport 'package:pigeon\u002Fpigeon.dart';\n\n@ConfigurePigeon(PigeonOptions(\n  dartOut: 'lib\u002Fplatform\u002Fdevice_api.g.dart',\n  kotlinOut:\n      'android\u002Fapp\u002Fsrc\u002Fmain\u002Fkotlin\u002Fdev\u002Fmiosito\u002Fapp\u002FDeviceApi.g.kt',\n  kotlinOptions: KotlinOptions(package: 'dev.miosito.app'),\n  swiftOut: 'ios\u002FRunner\u002FDeviceApi.g.swift',\n  dartPackageName: 'mia_app',\n))\nclass BatteryInfo {\n  BatteryInfo({required this.level, required this.isCharging});\n  final int level;\n  final bool isCharging;\n}\n\n@HostApi()\nabstract class DeviceApi {\n  BatteryInfo getBatteryInfo();\n  void setKeepScreenOn(bool enabled);\n}\n\n\u002F\u002F Uso lato Dart dopo la generazione:\n\u002F\u002F final api = DeviceApi();\n\u002F\u002F final info = await api.getBatteryInfo(); \u002F\u002F tipizzato, niente stringhe","Dopo `dart run pigeon` trovi i file generati in Dart, Kotlin e Swift: le chiamate native diventano type-safe e gli errori di contratto emergono a compile-time invece che in produzione.",[],1789120581818]