[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-bluetooth-low-energy-in-flutter-con-flutter-blue-plus-guida-pratica":3,"comments-article-bluetooth-low-energy-in-flutter-con-flutter-blue-plus-guida-pratica":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},76,"Bluetooth Low Energy in Flutter con flutter_blue_plus: guida pratica","bluetooth-low-energy-in-flutter-con-flutter-blue-plus-guida-pratica","Come dialogare con dispositivi BLE da un'app Flutter: permessi Android e iOS, scansione, connessione, scoperta di servizi e caratteristiche, notifiche e gestione robusta della disconnessione con flutter_blue_plus.","## Perché il BLE è diverso da tutto il resto\n\nSe sei abituato a chiamare API REST, il Bluetooth Low Energy ti costringerà a cambiare mentalità. Non esiste un \"server\" sempre disponibile: esiste un dispositivo fisico che può essere fuori portata, con la batteria scarica, già connesso a un altro telefono o semplicemente in uno stato inconsistente. Ogni operazione può fallire, e fallirà.\n\nIn Flutter il pacchetto di riferimento è **flutter_blue_plus**, fork mantenuto attivamente dello storico `flutter_blue`, che supporta Android, iOS, macOS e (parzialmente) Windows\u002FLinux. In questo articolo vediamo come costruire un livello BLE affidabile, dai permessi fino allo streaming di dati da una caratteristica.\n\n## Concetti fondamentali (in due minuti)\n\n- **Central \u002F Peripheral**: il telefono è tipicamente il *central*, il dispositivo (sensore, bilancia, braccialetto) è il *peripheral*.\n- **Advertising**: il peripheral trasmette pacchetti che il central rileva durante la scansione.\n- **GATT**: la struttura dati esposta dal peripheral una volta connessi.\n- **Service**: un gruppo logico di funzionalità, identificato da un UUID (es. `180D` per Heart Rate).\n- **Characteristic**: il singolo \"campo\" leggibile, scrivibile o notificabile all'interno di un service.\n- **Descriptor**: metadati di una caratteristica; il più noto è il CCCD, che abilita le notifiche.\n\nGli UUID standard sono a 16 bit (es. `2A37`), quelli custom a 128 bit.\n\n## Installazione e configurazione\n\n```yaml\ndependencies:\n  flutter_blue_plus: ^1.32.12\n  permission_handler: ^11.3.1\n```\n\n### Android\n\nSu Android 12+ i permessi Bluetooth sono runtime permission separate dalla posizione. In `android\u002Fapp\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`:\n\n```xml\n\u003Cuses-permission android:name=\"android.permission.BLUETOOTH_SCAN\"\n    android:usesPermissionFlags=\"neverForLocation\" \u002F>\n\u003Cuses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" \u002F>\n\n\u003C!-- Solo per Android 11 e precedenti -->\n\u003Cuses-permission android:name=\"android.permission.BLUETOOTH\"\n    android:maxSdkVersion=\"30\" \u002F>\n\u003Cuses-permission android:name=\"android.permission.BLUETOOTH_ADMIN\"\n    android:maxSdkVersion=\"30\" \u002F>\n\u003Cuses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"\n    android:maxSdkVersion=\"30\" \u002F>\n\n\u003Cuses-feature android:name=\"android.hardware.bluetooth_le\" android:required=\"false\" \u002F>\n```\n\nAttenzione: il flag `neverForLocation` è una dichiarazione formale. Se la tua app **deduce la posizione** dai beacon rilevati (es. iBeacon), devi rimuoverlo e richiedere `ACCESS_FINE_LOCATION` anche su Android 12+.\n\nServe inoltre `minSdkVersion 21` (meglio 23) in `android\u002Fapp\u002Fbuild.gradle`.\n\n### iOS\n\nIn `ios\u002FRunner\u002FInfo.plist`:\n\n```xml\n\u003Ckey>NSBluetoothAlwaysUsageDescription\u003C\u002Fkey>\n\u003Cstring>L'app usa il Bluetooth per collegarsi al tuo sensore.\u003C\u002Fstring>\n```\n\nSe non compili una descrizione sensata, Apple rifiuta la build in review. Ricorda anche che **il BLE non funziona sul simulatore iOS**: serve un dispositivo reale.\n\n## Verificare adapter e permessi\n\nPrima di scansionare, controlla che l'hardware sia presente e acceso. `FlutterBluePlus.adapterState` è uno stream: usalo per reagire quando l'utente spegne il Bluetooth mentre l'app è aperta.\n\n```dart\nimport 'package:flutter_blue_plus\u002Fflutter_blue_plus.dart';\nimport 'package:permission_handler\u002Fpermission_handler.dart';\n\nFuture\u003Cbool> prepareBle() async {\n  if (!await FlutterBluePlus.isSupported) return false;\n\n  \u002F\u002F Android: richiesta esplicita dei permessi runtime\n  final statuses = await [\n    Permission.bluetoothScan,\n    Permission.bluetoothConnect,\n  ].request();\n\n  if (statuses.values.any((s) => !s.isGranted)) return false;\n\n  \u002F\u002F Su Android possiamo chiedere all'utente di accendere l'adapter\n  if (FlutterBluePlus.adapterStateNow == BluetoothAdapterState.off) {\n    try {\n      await FlutterBluePlus.turnOn();\n    } catch (_) {\n      return false; \u002F\u002F non supportato su iOS\n    }\n  }\n\n  final state = await FlutterBluePlus.adapterState\n      .where((s) => s == BluetoothAdapterState.on)\n      .first\n      .timeout(const Duration(seconds: 10));\n\n  return state == BluetoothAdapterState.on;\n}\n```\n\n## Scansione dei dispositivi\n\nLa scansione è costosa in termini di batteria: filtra sempre e imposta un timeout.\n\n```dart\nfinal heartRateService = Guid('180D');\n\nStream\u003CList\u003CScanResult>> scanDevices() {\n  FlutterBluePlus.startScan(\n    withServices: [heartRateService], \u002F\u002F filtro lato sistema\n    timeout: const Duration(seconds: 15),\n    androidUsesFineLocation: false,\n  );\n  return FlutterBluePlus.scanResults;\n}\n```\n\nAlcune note importanti:\n\n- `withServices` filtra a livello di sistema operativo: molto più efficiente che filtrare in Dart, ma funziona solo se il dispositivo pubblicizza quel service nell'advertising.\n- Puoi usare anche `withNames`, `withKeywords` (solo Android) o `withRemoteIds` per riconnetterti a un dispositivo noto.\n- `FlutterBluePlus.scanResults` emette la **lista completa** aggiornata: non serve accumulare i risultati a mano.\n- Ferma sempre la scansione prima di connetterti: `await FlutterBluePlus.stopScan();`\n\nUn widget minimale:\n\n```dart\nStreamBuilder\u003CList\u003CScanResult>>(\n  stream: FlutterBluePlus.scanResults,\n  initialData: const [],\n  builder: (context, snapshot) {\n    final results = snapshot.data!\n      ..sort((a, b) => b.rssi.compareTo(a.rssi));\n    return ListView.builder(\n      itemCount: results.length,\n      itemBuilder: (context, i) {\n        final r = results[i];\n        return ListTile(\n          title: Text(r.device.platformName.isEmpty\n              ? 'Sconosciuto'\n              : r.device.platformName),\n          subtitle: Text(r.device.remoteId.str),\n          trailing: Text('${r.rssi} dBm'),\n          onTap: () => _connect(r.device),\n        );\n      },\n    );\n  },\n)\n```\n\nIl `remoteId` è il MAC address su Android e un UUID generato dal sistema su iOS: **non è portabile tra piattaforme**, ma è stabile sullo stesso dispositivo e va salvato per le riconnessioni rapide.\n\n## Connessione e ciclo di vita\n\n```dart\nFuture\u003Cvoid> _connect(BluetoothDevice device) async {\n  await FlutterBluePlus.stopScan();\n\n  \u002F\u002F Ascolta la connessione PRIMA di connetterti\n  final sub = device.connectionState.listen((state) {\n    debugPrint('Stato: $state');\n    if (state == BluetoothConnectionState.disconnected) {\n      debugPrint('Motivo: ${device.disconnectReason?.description}');\n    }\n  });\n  device.cancelWhenDisconnected(sub, delayed: true, next: true);\n\n  await device.connect(timeout: const Duration(seconds: 15));\n\n  \u002F\u002F Su Android un MTU maggiore riduce drasticamente i round trip\n  if (Theme.of(context).platform == TargetPlatform.android) {\n    await device.requestMtu(247);\n  }\n}\n```\n\n`cancelWhenDisconnected` è una comodità di flutter_blue_plus: annulla automaticamente la subscription quando il dispositivo si disconnette, evitando memory leak e listener zombie.\n\nPer riconnetterti in automatico quando il dispositivo torna in portata, su Android puoi usare:\n\n```dart\nawait device.connect(autoConnect: true, mtu: null);\nawait device.connectionState\n    .where((s) => s == BluetoothConnectionState.connected)\n    .first;\n```\n\nCon `autoConnect: true` la chiamata ritorna subito e il sistema tenta la connessione in background; per questo il parametro `mtu` deve essere `null`.\n\n## Scoprire servizi e caratteristiche\n\n```dart\nFuture\u003CBluetoothCharacteristic?> findCharacteristic(\n  BluetoothDevice device,\n  Guid serviceUuid,\n  Guid characteristicUuid,\n) async {\n  final services = await device.discoverServices();\n  for (final service in services) {\n    if (service.serviceUuid != serviceUuid) continue;\n    for (final c in service.characteristics) {\n      if (c.characteristicUuid == characteristicUuid) return c;\n    }\n  }\n  return null;\n}\n```\n\n`discoverServices()` va chiamata **dopo** la connessione e **prima** di qualsiasi operazione di lettura\u002Fscrittura. Su Android l'operazione può richiedere qualche secondo.\n\n## Leggere, scrivere e ricevere notifiche\n\n```dart\n\u002F\u002F Lettura one-shot\nfinal List\u003Cint> value = await characteristic.read();\n\n\u002F\u002F Scrittura con risposta (affidabile ma più lenta)\nawait characteristic.write([0x01, 0x0A], withoutResponse: false);\n\n\u002F\u002F Notifiche: il pattern più usato per i sensori\nfinal sub = characteristic.onValueReceived.listen((data) {\n  final bpm = _parseHeartRate(data);\n  debugPrint('Battito: $bpm');\n});\ndevice.cancelWhenDisconnected(sub);\n\nawait characteristic.setNotifyValue(true);\n```\n\nL'ordine conta: **iscriviti a `onValueReceived` prima di chiamare `setNotifyValue(true)`**, altrimenti rischi di perdere i primi pacchetti.\n\nIl parsing dei byte è a tuo carico. Usa `ByteData` per gestire endianness e tipi:\n\n```dart\nint _parseHeartRate(List\u003Cint> data) {\n  if (data.isEmpty) return 0;\n  final bytes = Uint8List.fromList(data);\n  final flags = bytes[0];\n  final isUint16 = (flags & 0x01) != 0;\n  final bd = ByteData.sublistView(bytes);\n  return isUint16 ? bd.getUint16(1, Endian.little) : bytes[1];\n}\n```\n\n## Incapsulare tutto in un repository\n\nSpargere chiamate a `FlutterBluePlus` nei widget è la ricetta per il caos. Meglio un repository che esponga uno stream tipizzato e nasconda i dettagli del protocollo.\n\n```dart\nclass HeartRateRepository {\n  static final _service = Guid('180D');\n  static final _measurement = Guid('2A37');\n\n  BluetoothDevice? _device;\n  StreamSubscription\u003CList\u003Cint>>? _valueSub;\n  final _controller = StreamController\u003Cint>.broadcast();\n\n  Stream\u003Cint> get heartRate => _controller.stream;\n\n  Future\u003Cvoid> connect(BluetoothDevice device) async {\n    _device = device;\n    await device.connect(timeout: const Duration(seconds: 15));\n\n    final services = await device.discoverServices();\n    final characteristic = services\n        .firstWhere((s) => s.serviceUuid == _service)\n        .characteristics\n        .firstWhere((c) => c.characteristicUuid == _measurement);\n\n    _valueSub = characteristic.onValueReceived\n        .map(_parseHeartRate)\n        .listen(_controller.add, onError: _controller.addError);\n    device.cancelWhenDisconnected(_valueSub!);\n\n    await characteristic.setNotifyValue(true);\n  }\n\n  Future\u003Cvoid> disconnect() async {\n    await _valueSub?.cancel();\n    await _device?.disconnect();\n    _device = null;\n  }\n\n  void dispose() {\n    _controller.close();\n  }\n}\n```\n\nDa qui il passo verso Riverpod, BLoC o qualsiasi altra soluzione di state management è breve: il repository espone solo `Stream\u003Cint>` e due metodi.\n\n## Gestire gli errori come si deve\n\nOgni operazione BLE può lanciare una `FlutterBluePlusException`, che contiene il codice di errore nativo:\n\n```dart\ntry {\n  await device.connect(timeout: const Duration(seconds: 15));\n} on FlutterBluePlusException catch (e) {\n  \u002F\u002F e.code: 133 su Android è il famigerato GATT_ERROR generico\n  if (e.code == 133) {\n    await Future.delayed(const Duration(seconds: 2));\n    await device.connect(); \u002F\u002F un retry risolve nella maggior parte dei casi\n  } else {\n    rethrow;\n  }\n} on TimeoutException {\n  \u002F\u002F dispositivo fuori portata\n}\n```\n\nStrategie che salvano ore di debugging:\n\n- **Serializza le operazioni**: non lanciare read\u002Fwrite in parallelo sullo stesso dispositivo. flutter_blue_plus mette già in coda le richieste, ma il tuo codice deve rispettare l'ordine logico.\n- **Retry con backoff** sulle connessioni: il codice 133 di Android è quasi sempre transitorio.\n- **Timeout ovunque**: nessuna Future BLE deve poter restare pendente all'infinito.\n- **Un solo punto di verità** sullo stato della connessione, altrimenti la UI mostrerà dati incoerenti.\n\n## Log e strumenti di debug\n\n```dart\nFlutterBluePlus.setLogLevel(LogLevel.verbose, color: true);\n```\n\nAffianca sempre un'app di riferimento come **nRF Connect** (Nordic Semiconductor) per verificare se il problema è nel tuo codice o nel firmware del dispositivo: se il service non compare nemmeno lì, non è colpa di Flutter.\n\n## Consumo energetico e background\n\nLa scansione continua è il modo più rapido per prosciugare la batteria. Alcune regole pratiche:\n\n- Scansiona per finestre brevi (10-15 secondi), poi fermati.\n- Dopo il primo accoppiamento salva il `remoteId` e usa `withRemoteIds` o `autoConnect` invece di riscansionare tutto.\n- Riduci la frequenza delle notifiche lato firmware quando possibile: è più efficiente di filtrare in Dart.\n- Per il background su iOS servono i *Background Modes* (`bluetooth-central`) e vincoli precisi: il sistema può sospendere l'app in qualsiasi momento. Su Android valuta un Foreground Service.\n\n## Conclusioni\n\nIl BLE in Flutter è pienamente fattibile, ma richiede disciplina: permessi corretti per ogni versione di Android, stream sottoscritti nell'ordine giusto, timeout e retry su ogni operazione, e un repository che isoli la complessità dal resto dell'app. Parti da un caso d'uso semplice (una caratteristica in notifica), verifica il comportamento su dispositivi reali di fascia bassa e solo dopo aggiungi riconnessione automatica e gestione del background.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002Fb86a5fab-a79b-4105-9442-b856f5ba808f.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1741454238936-0a40beef1db3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3ODc1NDQwODh8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"Samuel Angor","https:\u002F\u002Funsplash.com\u002F@sammysays___","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fclose-up-of-a-smart-watch-t8YSwEqdmbI",null,"published","2026-08-24T04:01:28+00:00","BLE in Flutter con flutter_blue_plus: guida pratica","Guida completa al Bluetooth Low Energy in Flutter con flutter_blue_plus: permessi Android e iOS, scansione, connessione, caratteristiche e notifiche.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1789205511287]