[{"data":1,"prerenderedAt":88},["ShallowReactive",2],{"tutorial-creare-un-plugin-flutter-federato-multipiattaforma-e-pubblicarlo-su-pubdev":3,"comments-tutorial-creare-un-plugin-flutter-federato-multipiattaforma-e-pubblicarlo-su-pubdev":87},{"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},106,"Creare un plugin Flutter federato multipiattaforma e pubblicarlo su pub.dev","creare-un-plugin-flutter-federato-multipiattaforma-e-pubblicarlo-su-pubdev","Guida avanzata all'architettura federata dei plugin Flutter: platform interface, implementazione MethodChannel, codice nativo Android\u002FiOS, supporto Web, endorsement e pubblicazione su pub.dev.","Quando un plugin Flutter deve funzionare su Android, iOS, Web e desktop, mettere tutto dentro un singolo package diventa rapidamente ingestibile: ogni nuova piattaforma obbliga a toccare il codice condiviso e chi mantiene una singola piattaforma deve avere i permessi sull'intero repository.\n\nLa risposta ufficiale del team Flutter è la **federated plugin architecture**: il plugin viene spezzato in più package con responsabilità separate.\n\n- **app-facing package**: l'API pubblica che importano gli sviluppatori (`device_battery`).\n- **platform interface package**: il contratto astratto che tutte le implementazioni devono rispettare (`device_battery_platform_interface`).\n- **platform implementation packages**: una per piattaforma (`device_battery_android`, `device_battery_ios`, `device_battery_web`, ...).\n\nIn questo tutorial costruiremo passo passo un plugin federato realistico che legge il livello della batteria e notifica i cambi di stato, con codice Kotlin, Swift e JS interop, test con mock della platform interface e pubblicazione su pub.dev.\n\n> **Prerequisiti**: buona conoscenza di Dart, dei MethodChannel\u002FEventChannel, di Gradle e di CocoaPods. Flutter 3.22 o superiore (Dart 3.4+, `dart:js_interop`).","https:\u002F\u002Fflutter.it\u002Fstorage\u002Ftutorials\u002Fa0acca0a-6e36-4da3-9ac6-b08a8a9a093b.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1524666643752-b381eb00effb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTAzOTcxMTZ8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Pankaj Patel","https:\u002F\u002Funsplash.com\u002F@pankajpatel","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fcloseup-photo-of-computer-code-screengrab-SXihyA4oEJs","https:\u002F\u002Fwww.youtube.com\u002Fwatch?v=t0ZisfvJ5ks",{"youtube_id":17,"duration_seconds":18,"published_at":19},"t0ZisfvJ5ks",172,"2026-09-26T09:39:11+00:00","advanced",75,"3.x (3.22+)","published","2026-09-26T04:31:56+00:00","Plugin Flutter federato: guida avanzata multipiattaforma","Crea un plugin Flutter federato: platform interface, MethodChannel, codice nativo Kotlin e Swift, supporto Web, endorsement e pubblicazione su pub.dev.",{"id":28,"name":29,"slug":30},1,"Guide","guide",{"id":28,"name":32},"Flutter Bot",[34,42,50,57,65,73,80],{"id":35,"position":28,"title":36,"body":37,"code_snippet":38,"code_language":39,"expected_result":40,"demo_url":41,"video_url":41},707,"Progettare la struttura del monorepo e generare gli scheletri","Partiamo creando una cartella radice che conterrà tutti i package del plugin. Useremo `flutter create` con il template `plugin` per le implementazioni native e con il template `package` per la platform interface (che è puro Dart più `plugin_platform_interface`).\n\nLa struttura finale sarà questa:\n\n```\ndevice_battery\u002F\n  packages\u002F\n    device_battery\u002F                      # app-facing\n    device_battery_platform_interface\u002F   # contratto\n    device_battery_android\u002F              # Kotlin\n    device_battery_ios\u002F                  # Swift\n    device_battery_web\u002F                  # JS interop\n```\n\nRegole di dipendenza da rispettare (sono il cuore dell'architettura):\n\n1. L'app-facing dipende **solo** dalla platform interface.\n2. Ogni implementazione dipende **solo** dalla platform interface.\n3. Nessuna implementazione dipende da un'altra implementazione né dall'app-facing.\n\nIn locale, per far vedere ai package le versioni non ancora pubblicate, usiamo `dependency_overrides` con path relativi: verranno ignorati in fase di pubblicazione ma permettono di sviluppare senza pubblicare nulla.","mkdir device_battery && cd device_battery && mkdir packages && cd packages\n\n# Contratto astratto (puro Dart)\nflutter create --template=package device_battery_platform_interface\n\n# App-facing package: non contiene codice nativo\nflutter create --template=plugin --platforms=android,ios,web \\\n  --org it.example --project-name device_battery device_battery\n\n# Implementazioni per piattaforma\nflutter create --template=plugin --platforms=android -a kotlin \\\n  --org it.example --project-name device_battery_android device_battery_android\nflutter create --template=plugin --platforms=ios -i swift \\\n  --org it.example --project-name device_battery_ios device_battery_ios\nflutter create --template=plugin --platforms=web \\\n  --org it.example --project-name device_battery_web device_battery_web","bash","Cinque package dentro `packages\u002F`, ognuno con il proprio `pubspec.yaml` e, dove previsto, le cartelle native `android\u002F` e `ios\u002F`.",null,{"id":43,"position":44,"title":45,"body":46,"code_snippet":47,"code_language":48,"expected_result":49,"demo_url":41,"video_url":41},708,2,"Definire la platform interface con PlatformInterface e token","La platform interface è un contratto astratto che **non** va implementato con `implements` ma con `extends`: in questo modo, aggiungendo un nuovo metodo in una minor release, le implementazioni esistenti continuano a compilare (ereditano il default che lancia `UnimplementedError`).\n\nIl meccanismo di sicurezza è il **token**: `PlatformInterface.verifyToken` impedisce che qualcuno implementi l'interfaccia con `implements`, evitando rotture silenziose.\n\nAggiungi al `pubspec.yaml` del package:\n\n```yaml\ndependencies:\n  flutter:\n    sdk: flutter\n  plugin_platform_interface: ^2.1.8\n```\n\nNel file `lib\u002Fdevice_battery_platform_interface.dart` definiamo il contratto, i modelli condivisi e l'implementazione di default basata su MethodChannel.","\u002F\u002F packages\u002Fdevice_battery_platform_interface\u002Flib\u002Fsrc\u002Fbattery_state.dart\nenum BatteryState { full, charging, discharging, unknown }\n\n\u002F\u002F packages\u002Fdevice_battery_platform_interface\u002Flib\u002Fsrc\u002Fdevice_battery_platform.dart\nimport 'package:plugin_platform_interface\u002Fplugin_platform_interface.dart';\n\nimport 'battery_state.dart';\nimport 'method_channel_device_battery.dart';\n\nabstract class DeviceBatteryPlatform extends PlatformInterface {\n  DeviceBatteryPlatform() : super(token: _token);\n\n  static final Object _token = Object();\n\n  static DeviceBatteryPlatform _instance = MethodChannelDeviceBattery();\n\n  static DeviceBatteryPlatform get instance => _instance;\n\n  \u002F\u002F\u002F Le implementazioni per piattaforma registrano qui se stesse.\n  static set instance(DeviceBatteryPlatform instance) {\n    PlatformInterface.verifyToken(instance, _token);\n    _instance = instance;\n  }\n\n  \u002F\u002F\u002F Livello batteria in percentuale (0-100).\n  Future\u003Cint> getBatteryLevel() {\n    throw UnimplementedError('getBatteryLevel() non implementato.');\n  }\n\n  \u002F\u002F\u002F Stream dei cambi di stato di carica.\n  Stream\u003CBatteryState> get onBatteryStateChanged {\n    throw UnimplementedError('onBatteryStateChanged non implementato.');\n  }\n}","dart","Il package espone `DeviceBatteryPlatform` con getter\u002Fsetter statici protetti da token e i metodi con default che lanciano `UnimplementedError`.",{"id":51,"position":52,"title":53,"body":54,"code_snippet":55,"code_language":48,"expected_result":56,"demo_url":41,"video_url":41},709,3,"Implementare il default basato su MethodChannel ed EventChannel","L'implementazione di default vive nella platform interface e serve da fallback: una piattaforma che parla lo stesso protocollo di canali non ha bisogno di scrivere Dart, le basta registrare la classe nativa.\n\nDue accortezze importanti:\n\n- **Visibilità per i test**: marca il channel con `@visibleForTesting` così i test possono intercettarlo.\n- **Broadcast stream**: l'`EventChannel.receiveBroadcastStream()` va creato una sola volta e memorizzato, altrimenti ogni `listen` apre un nuovo stream nativo.\n\nNormalizza sempre i dati provenienti dal lato nativo: le mappe arrivano come `Map\u003CObject?, Object?>` e i valori mancanti vanno gestiti esplicitamente.","\u002F\u002F packages\u002Fdevice_battery_platform_interface\u002Flib\u002Fsrc\u002Fmethod_channel_device_battery.dart\nimport 'package:flutter\u002Ffoundation.dart';\nimport 'package:flutter\u002Fservices.dart';\n\nimport 'battery_state.dart';\nimport 'device_battery_platform.dart';\n\nclass MethodChannelDeviceBattery extends DeviceBatteryPlatform {\n  @visibleForTesting\n  final MethodChannel methodChannel =\n      const MethodChannel('it.example\u002Fdevice_battery');\n\n  @visibleForTesting\n  final EventChannel eventChannel =\n      const EventChannel('it.example\u002Fdevice_battery\u002Fstate');\n\n  Stream\u003CBatteryState>? _stateStream;\n\n  @override\n  Future\u003Cint> getBatteryLevel() async {\n    final level = await methodChannel.invokeMethod\u003Cint>('getBatteryLevel');\n    if (level == null) {\n      throw PlatformException(\n        code: 'NO_DATA',\n        message: 'Il livello batteria non è disponibile su questo dispositivo.',\n      );\n    }\n    return level;\n  }\n\n  @override\n  Stream\u003CBatteryState> get onBatteryStateChanged {\n    return _stateStream ??= eventChannel\n        .receiveBroadcastStream()\n        .map((event) => _parseState(event as String?))\n        .asBroadcastStream();\n  }\n\n  BatteryState _parseState(String? raw) => switch (raw) {\n        'full' => BatteryState.full,\n        'charging' => BatteryState.charging,\n        'discharging' => BatteryState.discharging,\n        _ => BatteryState.unknown,\n      };\n}","`DeviceBatteryPlatform.instance` restituisce di default un `MethodChannelDeviceBattery` funzionante su qualsiasi piattaforma che implementi i due canali.",{"id":58,"position":59,"title":60,"body":61,"code_snippet":62,"code_language":63,"expected_result":64,"demo_url":41,"video_url":41},710,4,"Implementazione Android in Kotlin con registrazione dichiarativa","Nel package `device_battery_android` eliminiamo il file Dart generato da `flutter create` (non serve: riusiamo l'implementazione MethodChannel) e dichiariamo nel `pubspec.yaml` solo la `pluginClass` Android più il campo **`dartPluginClass`** se volessimo codice Dart specifico.\n\nIl `pubspec.yaml` dell'implementazione Android:\n\n```yaml\nname: device_battery_android\nflutter:\n  plugin:\n    implements: device_battery        # chiave dell'architettura federata\n    platforms:\n      android:\n        package: it.example.device_battery_android\n        pluginClass: DeviceBatteryPlugin\ndependencies:\n  flutter:\n    sdk: flutter\n  device_battery_platform_interface: ^1.0.0\n```\n\nLa direttiva `implements:` dice a Flutter che questo package è un'implementazione del plugin `device_battery`. Nel codice Kotlin implementiamo sia il `MethodCallHandler` sia lo `StreamHandler` per l'EventChannel, registrando un `BroadcastReceiver` per gli eventi di carica e deregistrandolo in `onCancel`\u002F`onDetachedFromEngine` per evitare leak.","package it.example.device_battery_android\n\nimport android.content.BroadcastReceiver\nimport android.content.Context\nimport android.content.Intent\nimport android.content.IntentFilter\nimport android.os.BatteryManager\nimport io.flutter.embedding.engine.plugins.FlutterPlugin\nimport io.flutter.plugin.common.EventChannel\nimport io.flutter.plugin.common.MethodCall\nimport io.flutter.plugin.common.MethodChannel\n\nclass DeviceBatteryPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,\n    EventChannel.StreamHandler {\n\n    private lateinit var context: Context\n    private lateinit var methodChannel: MethodChannel\n    private lateinit var eventChannel: EventChannel\n    private var receiver: BroadcastReceiver? = null\n\n    override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        context = binding.applicationContext\n        methodChannel = MethodChannel(binding.binaryMessenger, \"it.example\u002Fdevice_battery\")\n        methodChannel.setMethodCallHandler(this)\n        eventChannel = EventChannel(binding.binaryMessenger, \"it.example\u002Fdevice_battery\u002Fstate\")\n        eventChannel.setStreamHandler(this)\n    }\n\n    override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {\n        when (call.method) {\n            \"getBatteryLevel\" -> {\n                val manager = context.getSystemService(Context.BATTERY_SERVICE) as BatteryManager\n                val level = manager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)\n                if (level >= 0) result.success(level)\n                else result.error(\"NO_DATA\", \"Livello batteria non disponibile\", null)\n            }\n            else -> result.notImplemented()\n        }\n    }\n\n    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {\n        receiver = object : BroadcastReceiver() {\n            override fun onReceive(ctx: Context?, intent: Intent?) {\n                val status = intent?.getIntExtra(BatteryManager.EXTRA_STATUS, -1) ?: -1\n                val state = when (status) {\n                    BatteryManager.BATTERY_STATUS_FULL -> \"full\"\n                    BatteryManager.BATTERY_STATUS_CHARGING -> \"charging\"\n                    BatteryManager.BATTERY_STATUS_DISCHARGING -> \"discharging\"\n                    else -> \"unknown\"\n                }\n                events?.success(state)\n            }\n        }\n        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_BATTERY_CHANGED))\n    }\n\n    override fun onCancel(arguments: Any?) {\n        receiver?.let { context.unregisterReceiver(it) }\n        receiver = null\n    }\n\n    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {\n        methodChannel.setMethodCallHandler(null)\n        eventChannel.setStreamHandler(null)\n        onCancel(null)\n    }\n}","kotlin","Su Android l'app di esempio legge il livello batteria e riceve gli eventi di carica senza che l'app-facing package conosca il codice nativo.",{"id":66,"position":67,"title":68,"body":69,"code_snippet":70,"code_language":71,"expected_result":72,"demo_url":41,"video_url":41},711,5,"Implementazione iOS in Swift e implementazione Web con dart:js_interop","**iOS**: il `pubspec.yaml` di `device_battery_ios` dichiara `implements: device_battery` e la `pluginClass: DeviceBatteryPlugin`. Ricorda di abilitare `UIDevice.current.isBatteryMonitoringEnabled = true`, altrimenti `batteryLevel` restituisce `-1`.\n\n**Web**: qui non esistono MethodChannel nativi, quindi l'implementazione Dart è obbligatoria. Si usa il campo `pluginClass` insieme a `fileName` nel `pubspec.yaml`:\n\n```yaml\nname: device_battery_web\nflutter:\n  plugin:\n    implements: device_battery\n    platforms:\n      web:\n        pluginClass: DeviceBatteryWeb\n        fileName: device_battery_web.dart\n```\n\nFlutter genera automaticamente il registrant che chiama il metodo statico `registerWith(Registrar)`: è lì che sovrascriviamo `DeviceBatteryPlatform.instance`. Per parlare con l'API `navigator.getBattery()` usiamo `dart:js_interop` con le extension type (Dart 3.3+), evitando il deprecato `dart:html`.","\u002F\u002F packages\u002Fdevice_battery_ios\u002Fios\u002FClasses\u002FDeviceBatteryPlugin.swift\nimport Flutter\nimport UIKit\n\npublic class DeviceBatteryPlugin: NSObject, FlutterPlugin {\n  public static func register(with registrar: FlutterPluginRegistrar) {\n    let channel = FlutterMethodChannel(name: \"it.example\u002Fdevice_battery\",\n                                       binaryMessenger: registrar.messenger())\n    let instance = DeviceBatteryPlugin()\n    registrar.addMethodCallDelegate(instance, channel: channel)\n  }\n\n  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {\n    switch call.method {\n    case \"getBatteryLevel\":\n      let device = UIDevice.current\n      device.isBatteryMonitoringEnabled = true\n      if device.batteryLevel \u003C 0 {\n        result(FlutterError(code: \"NO_DATA\", message: \"Batteria non disponibile\", details: nil))\n      } else {\n        result(Int(device.batteryLevel * 100))\n      }\n    default:\n      result(FlutterMethodNotImplemented)\n    }\n  }\n}\n\n\u002F* -------- packages\u002Fdevice_battery_web\u002Flib\u002Fdevice_battery_web.dart --------\nimport 'dart:js_interop';\n\nimport 'package:device_battery_platform_interface\u002Fdevice_battery_platform_interface.dart';\nimport 'package:flutter_web_plugins\u002Fflutter_web_plugins.dart';\n\n@JS('navigator.getBattery')\nexternal JSPromise\u003C_BatteryManager> _getBattery();\n\nextension type _BatteryManager(JSObject _) implements JSObject {\n  external double get level;\n  external bool get charging;\n}\n\nclass DeviceBatteryWeb extends DeviceBatteryPlatform {\n  static void registerWith(Registrar registrar) {\n    DeviceBatteryPlatform.instance = DeviceBatteryWeb();\n  }\n\n  @override\n  Future\u003Cint> getBatteryLevel() async {\n    final manager = await _getBattery().toDart;\n    return (manager.level * 100).round();\n  }\n\n  @override\n  Stream\u003CBatteryState> get onBatteryStateChanged async* {\n    final manager = await _getBattery().toDart;\n    yield manager.charging ? BatteryState.charging : BatteryState.discharging;\n  }\n}\n-------------------------------------------------------------------------- *\u002F","swift","Su iOS il plugin risponde via MethodChannel; su Web `DeviceBatteryWeb.registerWith` viene invocato automaticamente all'avvio e sostituisce l'istanza di default.",{"id":74,"position":75,"title":76,"body":77,"code_snippet":78,"code_language":48,"expected_result":79,"demo_url":41,"video_url":41},712,6,"Scrivere l'app-facing package ed effettuare l'endorsement","L'app-facing package è volutamente sottile: espone un'API ergonomica e delega tutto a `DeviceBatteryPlatform.instance`. Non deve contenere logica di piattaforma né `if (Platform.isAndroid)`.\n\nIl punto cruciale è l'**endorsement**: nel suo `pubspec.yaml` l'app-facing dichiara quali package implementano ciascuna piattaforma. Così l'utente finale aggiunge solo `device_battery` e le implementazioni vengono risolte automaticamente da pub.\n\n```yaml\nname: device_battery\nversion: 1.0.0\nflutter:\n  plugin:\n    platforms:\n      android:\n        default_package: device_battery_android\n      ios:\n        default_package: device_battery_ios\n      web:\n        default_package: device_battery_web\ndependencies:\n  flutter:\n    sdk: flutter\n  device_battery_platform_interface: ^1.0.0\n  device_battery_android: ^1.0.0\n  device_battery_ios: ^1.0.0\n  device_battery_web: ^1.0.0\n\ndependency_overrides:   # solo in sviluppo locale\n  device_battery_platform_interface:\n    path: ..\u002Fdevice_battery_platform_interface\n  device_battery_android:\n    path: ..\u002Fdevice_battery_android\n```\n\nSe un domani qualcuno pubblica `device_battery_windows` senza il tuo coinvolgimento, l'utente potrà aggiungerlo manualmente: è il vantaggio dell'architettura federata (implementazione **non endorsed**).","\u002F\u002F packages\u002Fdevice_battery\u002Flib\u002Fdevice_battery.dart\nimport 'package:device_battery_platform_interface\u002Fdevice_battery_platform_interface.dart';\n\nexport 'package:device_battery_platform_interface\u002Fdevice_battery_platform_interface.dart'\n    show BatteryState;\n\n\u002F\u002F\u002F API pubblica del plugin: nessun riferimento a Platform o ai channel.\nclass DeviceBattery {\n  const DeviceBattery();\n\n  DeviceBatteryPlatform get _platform => DeviceBatteryPlatform.instance;\n\n  \u002F\u002F\u002F Livello batteria in percentuale (0-100).\n  Future\u003Cint> get level => _platform.getBatteryLevel();\n\n  \u002F\u002F\u002F Stream dei cambi di stato di carica.\n  Stream\u003CBatteryState> get stateChanges => _platform.onBatteryStateChanged;\n\n  \u002F\u002F\u002F Helper di alto livello: true se la batteria è sotto la soglia indicata.\n  Future\u003Cbool> isLow({int threshold = 20}) async => await level \u003C= threshold;\n}","Nell'app di esempio basta `import 'package:device_battery\u002Fdevice_battery.dart';` e `await const DeviceBattery().level` per ottenere il valore su Android, iOS e Web.",{"id":81,"position":82,"title":83,"body":84,"code_snippet":85,"code_language":48,"expected_result":86,"demo_url":41,"video_url":41},713,7,"Testare con mock della platform interface e pubblicare su pub.dev","### Test\n\nPer testare l'app-facing package **non** serve un dispositivo: basta sostituire `DeviceBatteryPlatform.instance` con un fake che usa `MockPlatformInterfaceMixin` (aggira la verifica del token, pensato proprio per i test). Per l'implementazione MethodChannel, invece, intercetta il channel con `TestDefaultBinaryMessengerBinding`.\n\n### Checklist prima della pubblicazione\n\n1. `description` di almeno 60 caratteri, `homepage`\u002F`repository` valorizzati e `LICENSE` presente in ogni package.\n2. `CHANGELOG.md` con la versione corrente in cima.\n3. **Rimuovi i `dependency_overrides`** prima di pubblicare, altrimenti pub li segnala.\n4. Ordine di pubblicazione: prima la platform interface, poi le implementazioni, infine l'app-facing (ognuno deve trovare le dipendenze già online).\n5. Verifica ogni package con `dart pub publish --dry-run` e controlla il punteggio previsto con `dart pub global run pana`.\n\n### Versionamento\n\nLa platform interface segue regole severe: aggiungere un metodo con implementazione di default è una **minor**; renderlo astratto o cambiarne la firma è una **major** che obbliga a pubblicare nuove major di tutte le implementazioni. Documenta questa policy nel README del package.","\u002F\u002F packages\u002Fdevice_battery\u002Ftest\u002Fdevice_battery_test.dart\nimport 'package:device_battery\u002Fdevice_battery.dart';\nimport 'package:device_battery_platform_interface\u002Fdevice_battery_platform_interface.dart';\nimport 'package:flutter_test\u002Fflutter_test.dart';\nimport 'package:plugin_platform_interface\u002Fplugin_platform_interface.dart';\n\nclass _FakeDeviceBattery extends DeviceBatteryPlatform\n    with MockPlatformInterfaceMixin {\n  _FakeDeviceBattery(this.value);\n  final int value;\n\n  @override\n  Future\u003Cint> getBatteryLevel() async => value;\n\n  @override\n  Stream\u003CBatteryState> get onBatteryStateChanged =>\n      Stream.value(BatteryState.charging);\n}\n\nvoid main() {\n  TestWidgetsFlutterBinding.ensureInitialized();\n\n  test('level delega alla platform interface', () async {\n    DeviceBatteryPlatform.instance = _FakeDeviceBattery(42);\n    expect(await const DeviceBattery().level, 42);\n  });\n\n  test('isLow usa la soglia indicata', () async {\n    DeviceBatteryPlatform.instance = _FakeDeviceBattery(15);\n    expect(await const DeviceBattery().isLow(), isTrue);\n    expect(await const DeviceBattery().isLow(threshold: 10), isFalse);\n  });\n\n  test('una implementazione con implements viene rifiutata', () {\n    expect(\n      () => DeviceBatteryPlatform.instance = _BadImplementation(),\n      throwsA(isA\u003CAssertionError>()),\n    );\n  });\n}\n\n\u002F\u002F Implementa (male) il contratto: viene bloccata dal token.\nclass _BadImplementation implements DeviceBatteryPlatform {\n  @override\n  noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);\n}","`flutter test` passa in tutti i package, `dart pub publish --dry-run` non segnala warning e i cinque package sono pronti per essere pubblicati nell'ordine corretto su pub.dev.",[],1792316929166]