[{"data":1,"prerenderedAt":27},["ShallowReactive",2],{"articolo-nfc-in-flutter-con-nfc-manager-leggere-e-scrivere-tag-ndef":3,"comments-article-nfc-in-flutter-con-nfc-manager-leggere-e-scrivere-tag-ndef":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},114,"NFC in Flutter con nfc_manager: leggere e scrivere tag NDEF","nfc-in-flutter-con-nfc-manager-leggere-e-scrivere-tag-ndef","Guida pratica all'uso dell'NFC in Flutter: configurazione di Android e iOS, lettura e scrittura di tag NDEF con nfc_manager, decodifica dei record, gestione delle sessioni e best practice per un'esperienza utente affidabile.","## Perché l'NFC è ancora rilevante\n\nBiglietti d'ingresso, badge aziendali, etichette smart su prodotti, check-in in palestra, tracciabilità in magazzino: l'NFC (Near Field Communication) resta una tecnologia economica e immediata per collegare il mondo fisico a un'app mobile. Un tag NFC costa pochi centesimi, non ha batteria e può essere letto in meno di un secondo semplicemente avvicinando il telefono.\n\nIn Flutter il pacchetto di riferimento è **nfc_manager**, che espone sia l'API Android (`android.nfc`) sia quella iOS (Core NFC) con una superficie comune. In questo articolo vediamo come leggere e scrivere tag NDEF, come gestire correttamente il ciclo di vita della sessione e quali differenze di piattaforma non si possono ignorare.\n\n## NDEF in due minuti\n\nQuasi tutti i tag \"consumer\" (NTAG213\u002F215\u002F216, MIFARE Ultralight, ICODE SLIX) supportano il formato **NDEF** (NFC Data Exchange Format). Un messaggio NDEF è una lista di **record**, ognuno composto da:\n\n- un **TNF** (Type Name Format), che indica come interpretare il tipo;\n- un **type** (ad esempio `T` per testo, `U` per URI);\n- un **payload** di byte.\n\nI casi più comuni sono il record di testo, il record URI e il record MIME. Lavorare a livello NDEF è la scelta giusta nel 90% dei progetti: è portabile fra Android e iOS e non richiede di dialogare con i comandi proprietari del chip.\n\n## Installazione e configurazione\n\nAggiungi la dipendenza:\n\n```yaml\ndependencies:\n  nfc_manager: ^3.5.0\n```\n\n> Nota: la major 4.x del pacchetto ha introdotto API separate per piattaforma (`NfcManagerAndroid`, `NfcManagerIos`, `NdefAndroid`, `NdefIos`). Gli esempi di questo articolo usano la 3.x, ancora la più diffusa; se adotti la 4.x la logica resta la stessa ma cambiano i nomi delle classi.\n\n### Android\n\nIn `android\u002Fapp\u002Fsrc\u002Fmain\u002FAndroidManifest.xml`:\n\n```xml\n\u003Cuses-permission android:name=\"android.permission.NFC\" \u002F>\n\u003C!-- required=\"false\" evita di escludere dal Play Store i device senza NFC -->\n\u003Cuses-feature android:name=\"android.hardware.nfc\" android:required=\"false\" \u002F>\n```\n\nNessun permesso a runtime: su Android l'NFC non passa dal sistema dei permessi, basta che sia attivo nelle impostazioni.\n\n### iOS\n\nSu iOS servono tre passaggi:\n\n1. Abilitare la capability **Near Field Communication Tag Reading** nel target Runner (Xcode → Signing & Capabilities). Questo aggiunge l'entitlement:\n\n```xml\n\u003Ckey>com.apple.developer.nfc.readersession.formats\u003C\u002Fkey>\n\u003Carray>\n  \u003Cstring>NDEF\u003C\u002Fstring>\n  \u003Cstring>TAG\u003C\u002Fstring>\n\u003C\u002Farray>\n```\n\n2. Aggiungere la descrizione d'uso in `Info.plist`:\n\n```xml\n\u003Ckey>NFCReaderUsageDescription\u003C\u002Fkey>\n\u003Cstring>L'app usa l'NFC per leggere i tag dei prodotti.\u003C\u002Fstring>\n```\n\n3. Verificare che l'App ID sul portale Apple Developer abbia la capability NFC attiva, altrimenti la firma fallisce.\n\nL'NFC su iOS richiede iPhone 7 o superiore e **non funziona in simulatore**: serve un device fisico.\n\n## Verificare la disponibilità\n\nPrima di qualunque operazione, controlla se l'hardware c'è ed è attivo:\n\n```dart\nimport 'package:nfc_manager\u002Fnfc_manager.dart';\n\nFuture\u003Cbool> get isNfcReady => NfcManager.instance.isAvailable();\n```\n\nSu Android `isAvailable()` restituisce `false` anche quando il chip è presente ma disattivato: in quel caso ha senso mostrare un messaggio che inviti l'utente ad attivarlo dalle impostazioni di sistema.\n\n## Leggere un tag\n\nLa lettura avviene aprendo una **sessione**. Su Android la sessione attiva il reader mode in foreground; su iOS il sistema mostra automaticamente un foglio modale con il messaggio che hai configurato.\n\n```dart\nFuture\u003Cvoid> readTag() async {\n  if (!await NfcManager.instance.isAvailable()) {\n    throw StateError('NFC non disponibile o disattivato');\n  }\n\n  await NfcManager.instance.startSession(\n    pollingOptions: {\n      NfcPollingOption.iso14443, \u002F\u002F NTAG, MIFARE, carte\n      NfcPollingOption.iso15693, \u002F\u002F tag a lunga distanza\n    },\n    alertMessage: 'Avvicina il tag alla parte alta del telefono',\n    onDiscovered: (NfcTag tag) async {\n      final ndef = Ndef.from(tag);\n\n      if (ndef == null) {\n        await NfcManager.instance.stopSession(\n          errorMessage: 'Questo tag non è compatibile',\n        );\n        return;\n      }\n\n      final message = ndef.cachedMessage;\n      final testi = (message?.records ?? [])\n          .map(decodeTextRecord)\n          .whereType\u003CString>()\n          .toList();\n\n      debugPrint('Record letti: $testi');\n\n      await NfcManager.instance.stopSession(alertMessage: 'Lettura completata');\n    },\n  );\n}\n```\n\nPunti chiave:\n\n- `cachedMessage` contiene il messaggio già letto durante la scoperta del tag: evita una `read()` aggiuntiva ed è più veloce.\n- `stopSession` su iOS chiude il foglio modale mostrando un segno di spunta (`alertMessage`) o un errore (`errorMessage`). **Chiamarlo sempre**: una sessione lasciata aperta scade dopo circa 60 secondi con un errore poco elegante.\n- `onDiscovered` può essere invocata più volte se l'utente appoggia più tag: progetta la callback per essere idempotente.\n\n### Decodificare un record di testo\n\nIl pacchetto fornisce i costruttori per scrivere, ma la decodifica è a carico nostro. Il payload di un record di testo ha una struttura precisa: un byte di stato, il codice lingua, poi il testo.\n\n```dart\nimport 'dart:convert';\n\nString? decodeTextRecord(NdefRecord record) {\n  final isWellKnown =\n      record.typeNameFormat == NdefTypeNameFormat.nfcWellknown;\n  final isText = record.type.length == 1 && record.type.first == 0x54; \u002F\u002F 'T'\n  if (!isWellKnown || !isText) return null;\n\n  final payload = record.payload;\n  if (payload.isEmpty) return null;\n\n  final status = payload.first;\n  final isUtf16 = (status & 0x80) != 0;\n  final langLength = status & 0x3F;\n  final bytes = payload.sublist(1 + langLength);\n\n  return isUtf16\n      ? String.fromCharCodes(bytes) \u002F\u002F semplificazione: UTF-16 è raro\n      : utf8.decode(bytes, allowMalformed: true);\n}\n```\n\nPer i record URI (`type` uguale a `0x55`) il primo byte del payload è un **prefisso abbreviato** (`0x01` = `http:\u002F\u002Fwww.`, `0x03` = `http:\u002F\u002F`, `0x04` = `https:\u002F\u002F`, …) seguito dal resto dell'URL in UTF-8.\n\n## Scrivere su un tag\n\nLa scrittura segue lo stesso schema, con due controlli obbligatori: che il tag sia scrivibile e che il messaggio ci stia.\n\n```dart\nFuture\u003Cvoid> writeTag({required String testo, required Uri url}) async {\n  final message = NdefMessage([\n    NdefRecord.createText(testo),\n    NdefRecord.createUri(url),\n  ]);\n\n  await NfcManager.instance.startSession(\n    pollingOptions: {NfcPollingOption.iso14443},\n    alertMessage: 'Avvicina il tag da programmare',\n    onDiscovered: (tag) async {\n      final ndef = Ndef.from(tag);\n\n      if (ndef == null || !ndef.isWritable) {\n        await NfcManager.instance.stopSession(\n          errorMessage: 'Tag non scrivibile',\n        );\n        return;\n      }\n\n      if (message.byteLength > ndef.maxSize) {\n        await NfcManager.instance.stopSession(\n          errorMessage: 'Messaggio troppo grande '\n              '(${message.byteLength}\u002F${ndef.maxSize} byte)',\n        );\n        return;\n      }\n\n      try {\n        await ndef.write(message);\n        await NfcManager.instance.stopSession(alertMessage: 'Tag scritto');\n      } catch (e) {\n        await NfcManager.instance.stopSession(\n          errorMessage: 'Scrittura fallita: $e',\n        );\n      }\n    },\n  );\n}\n```\n\nSe vuoi rendere il contenuto permanente esiste `ndef.writeLock()`: attenzione, l'operazione è **irreversibile** e trasforma il tag in sola lettura per sempre. Usala solo in produzione, mai durante i test.\n\n## Incapsulare tutto in un servizio\n\nLavorare direttamente con `NfcManager` dentro i widget porta rapidamente a codice fragile. Conviene isolare la logica in un servizio che espone `Future` e gestisce l'annullamento.\n\n```dart\nclass NfcService {\n  bool _sessionActive = false;\n\n  Future\u003CT> scan\u003CT>(\n    Future\u003CT> Function(NfcTag tag) handler, {\n    String alertMessage = 'Avvicina il tag',\n    Duration timeout = const Duration(seconds: 30),\n  }) async {\n    if (_sessionActive) throw StateError('Sessione NFC già attiva');\n    if (!await NfcManager.instance.isAvailable()) {\n      throw const NfcUnavailableException();\n    }\n\n    final completer = Completer\u003CT>();\n    _sessionActive = true;\n\n    await NfcManager.instance.startSession(\n      pollingOptions: {NfcPollingOption.iso14443, NfcPollingOption.iso15693},\n      alertMessage: alertMessage,\n      onDiscovered: (tag) async {\n        if (completer.isCompleted) return;\n        try {\n          final result = await handler(tag);\n          completer.complete(result);\n          await NfcManager.instance.stopSession(alertMessage: 'Fatto');\n        } catch (e, s) {\n          completer.completeError(e, s);\n          await NfcManager.instance.stopSession(errorMessage: '$e');\n        }\n      },\n    );\n\n    try {\n      return await completer.future.timeout(timeout);\n    } finally {\n      _sessionActive = false;\n      await cancel();\n    }\n  }\n\n  Future\u003Cvoid> cancel() async {\n    try {\n      await NfcManager.instance.stopSession();\n    } catch (_) {\n      \u002F\u002F la sessione potrebbe essere già chiusa\n    }\n  }\n}\n\nclass NfcUnavailableException implements Exception {\n  const NfcUnavailableException();\n  @override\n  String toString() => 'NFC non disponibile';\n}\n```\n\nIl servizio diventa facilmente testabile sostituendo `NfcManager` con un'astrazione, e nella UI il codice si riduce a:\n\n```dart\nfinal badge = await nfcService.scan((tag) async {\n  final ndef = Ndef.from(tag);\n  final records = ndef?.cachedMessage?.records ?? const [];\n  return records.map(decodeTextRecord).whereType\u003CString>().firstOrNull;\n});\n```\n\n## Collegare la sessione al ciclo di vita del widget\n\nUna sessione aperta mentre l'utente abbandona la schermata è una fonte classica di bug, soprattutto su iOS dove il foglio modale resta visibile. Chiudila nel `dispose` e quando l'app va in background:\n\n```dart\nclass ScanPage extends StatefulWidget {\n  const ScanPage({super.key});\n  @override\n  State\u003CScanPage> createState() => _ScanPageState();\n}\n\nclass _ScanPageState extends State\u003CScanPage> {\n  final _nfc = NfcService();\n  late final AppLifecycleListener _listener;\n\n  @override\n  void initState() {\n    super.initState();\n    _listener = AppLifecycleListener(\n      onPause: () => _nfc.cancel(),\n    );\n  }\n\n  @override\n  void dispose() {\n    _nfc.cancel();\n    _listener.dispose();\n    super.dispose();\n  }\n\n  \u002F\u002F build...\n}\n```\n\n## Differenze di piattaforma da conoscere\n\n| Aspetto | Android | iOS |\n|---|---|---|\n| UI della scansione | a carico dell'app | foglio di sistema non personalizzabile (solo testo) |\n| Durata sessione | finché non la chiudi | ~60 secondi, poi timeout |\n| Lettura in background | sì, via intent filter nel manifest | solo tag NDEF con URL universal link, su device recenti |\n| Emulazione carta (HCE) | supportata | non disponibile alle app di terze parti |\n| Comandi proprietari | ampio accesso (MIFARE, NfcA, IsoDep) | limitato ai formati dichiarati nell'entitlement |\n\nSu Android puoi far aprire l'app quando l'utente appoggia un tag anche a app chiusa, dichiarando un intent filter:\n\n```xml\n\u003Cintent-filter>\n  \u003Caction android:name=\"android.nfc.action.NDEF_DISCOVERED\" \u002F>\n  \u003Ccategory android:name=\"android.intent.category.DEFAULT\" \u002F>\n  \u003Cdata android:scheme=\"https\" android:host=\"esempio.it\" android:pathPrefix=\"\u002Ftag\" \u002F>\n\u003C\u002Fintent-filter>\n```\n\nIn questo caso il tag contiene un URL e il flusso si sovrappone a quello del deep linking: l'app riceve l'intent come un normale link e puoi gestirlo con il tuo router.\n\n## Oltre NDEF: comandi a basso livello\n\nQuando devi dialogare con carte proprietarie (MIFARE Classic, DESFire, carte ISO 7816) NDEF non basta. `nfc_manager` espone classi specifiche come `MifareUltralight.from(tag)`, `NfcA.from(tag)`, `IsoDep.from(tag)` su Android e `MiFare.from(tag)` \u002F `Iso7816.from(tag)` su iOS, con metodi per inviare APDU o comandi raw:\n\n```dart\nfinal isoDep = IsoDep.from(tag);\nif (isoDep != null) {\n  final response = await isoDep.transceive(\n    data: Uint8List.fromList([0x00, 0xA4, 0x04, 0x00]),\n  );\n}\n```\n\nIn questo scenario il codice diventa inevitabilmente specifico per piattaforma: conviene isolarlo dietro un'interfaccia e testarlo su hardware reale. Ricorda inoltre di aggiungere gli `com.apple.developer.nfc.readersession.iso7816.select-identifiers` nell'`Info.plist` per gli AID che vuoi selezionare su iOS.\n\n## Sicurezza: un tag non è una credenziale\n\nIl contenuto NDEF di un tag è **in chiaro e clonabile** con qualunque smartphone. Non usare mai l'UID di un tag come unico fattore di autenticazione: gli UID possono essere emulati con tag \"magic\" o con dispositivi dedicati.\n\nLinee guida pratiche:\n\n- fai firmare il payload lato server (HMAC) e verifica la firma nell'app o, meglio ancora, sul backend;\n- includi un contatore anti-replay (molti NTAG supportano la funzione **SUN\u002Fmirror** con contatore e CMAC);\n- tratta l'NFC come un *identificatore*, non come una *prova di identità*;\n- per scenari ad alta sicurezza usa carte con autenticazione crittografica (DESFire EV2\u002FEV3).\n\n## Checklist finale\n\n- Verifica sempre `isAvailable()` e mostra un fallback (per esempio uno scanner QR) sui device senza NFC.\n- Chiudi la sessione in ogni percorso di codice, compresi gli errori e il `dispose`.\n- Imposta messaggi chiari in `alertMessage`: su iOS sono l'unica UI che l'utente vede.\n- Controlla `isWritable` e `maxSize` prima di scrivere; un NTAG213 offre solo ~137 byte utili.\n- Testa su hardware reale: emulatori e simulatori non supportano l'NFC.\n- Gestisci il caso \"tag rimosso troppo presto\": è l'errore più frequente in produzione, meglio invitare l'utente a tenere fermo il telefono.\n\nCon poche decine di righe e un servizio ben incapsulato, l'NFC diventa una funzionalità solida e piacevole da usare, capace di eliminare digitazioni, codici e attriti dai flussi della tua app Flutter.","https:\u002F\u002Fflutter.it\u002Fstorage\u002Farticles\u002F39f45c12-1cf2-4aed-864c-c98cc7b7993a.jpg","https:\u002F\u002Fimages.unsplash.com\u002Fphoto-1726066012645-959fc63f61b4?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w5NzA2NTJ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3OTA4MjczMTF8&ixlib=rb-4.1.0&q=80&w=1080",{"name":12,"author_url":13,"photo_url":14},"SumUp","https:\u002F\u002Funsplash.com\u002F@sumup","https:\u002F\u002Funsplash.com\u002Fphotos\u002Fa-person-holding-a-cell-phone-in-their-hand-YY3vVdjyx14",null,"published","2026-10-01T04:01:51+00:00","NFC in Flutter con nfc_manager: leggere e scrivere tag","Guida pratica all'NFC in Flutter: setup Android e iOS, lettura e scrittura di tag NDEF con nfc_manager, gestione sessioni, sicurezza e best practice.",{"id":21,"name":22,"slug":23},1,"Guide","guide",{"id":21,"name":25},"Flutter Bot",[],1792316908070]