DEV Community

Cover image for Encoding NDEF Records by Hand in Flutter — URIs, Text, vCards and Wi-Fi
Ayesha Iftikhar
Ayesha Iftikhar

Posted on

Encoding NDEF Records by Hand in Flutter — URIs, Text, vCards and Wi-Fi

Most Flutter NFC tutorials stop at "write a URL". When I built the encoding layer for an NFC app, I needed eleven record types, exact byte counts before writing, and records that every phone would read. Packages like nfc_manager give you the session and the tag; building the right bytes is still up to you.

This post walks through four record types — URI, text, vCard and Wi-Fi — at the byte level, with the Dart I ended up with.

A 30-second NDEF refresher

An NDEF message is a list of records. Each record has:

  • a TNF (Type Name Format): well-known (NFC Forum types like U and T), media (a MIME type), external, and so on
  • a type (e.g. U, or text/vcard)
  • an optional ID
  • a payload

With ndef_record/nfc_manager_ndef, building one looks like this:

ndef.NdefRecord(
  typeNameFormat: ndef.TypeNameFormat.wellKnown,
  type: Uint8List.fromList([0x55]), // 'U'
  identifier: Uint8List(0),
  payload: payloadBytes,
);
Enter fullscreen mode Exit fullscreen mode

The interesting part is always the payload.

1. URI records: the one-byte prefix trick

The NFC Forum URI record (U, 0x55) starts its payload with a prefix code: one byte that stands for a common URI start. 0x04 means https://, 0x02 means https://www., 0x05 means tel:, 0x06 means mailto: — 36 entries in all, index 0 being "no prefix".

On an NTAG213 with 144 bytes of user memory, saving 12 bytes on https://www. matters.

const uriPrefixList = [
  '', 'http://www.', 'https://www.', 'http://', 'https://',
  'tel:', 'mailto:', /* … the rest of the NFC Forum table … */
];

static ndef.NdefRecord encodeUri(String uri) {
  var rest = uri;
  var prefixIndex = 0;
  for (var i = 1; i < uriPrefixList.length; i++) {
    if (rest.startsWith(uriPrefixList[i])) {
      prefixIndex = i;
      rest = rest.substring(uriPrefixList[i].length);
      break;
    }
  }
  return ndef.NdefRecord(
    typeNameFormat: ndef.TypeNameFormat.wellKnown,
    type: Uint8List.fromList([0x55]),
    identifier: Uint8List(0),
    payload: Uint8List.fromList([prefixIndex, ...utf8.encode(rest)]),
  );
}
Enter fullscreen mode Exit fullscreen mode

Gotcha: order matters. https://www. must be checked before https://. The table's own order puts the www. variants first, so walking it in order does the right thing.

The same encoder handles phone (tel:), SMS (sms:), email (mailto:), social profiles and map locations (geo:lat,lng), because they're all just URIs.

2. Text records: a status byte you can't skip

Text records (T, 0x54) start with a status byte: bit 7 = encoding (0 for UTF-8), low 6 bits = length of the language code that follows. Then the language code, then the text.

static ndef.NdefRecord encodeText(String text) {
  final lang = utf8.encode('en');
  final status = lang.length; // bit 7 clear = UTF-8
  return ndef.NdefRecord(
    typeNameFormat: ndef.TypeNameFormat.wellKnown,
    type: Uint8List.fromList([0x54]),
    identifier: Uint8List(0),
    payload: Uint8List.fromList([status, ...lang, ...utf8.encode(text)]),
  );
}
Enter fullscreen mode Exit fullscreen mode

If you forget the status byte, some readers show the first character of your text as garbage, and others drop the record entirely.

3. vCards: just a media record

Contact cards aren't an NFC Forum type. They're a media record whose type is the MIME string text/vcard and whose payload is the vCard text:

static ndef.NdefRecord encodeMedia(String mimeType, String data) {
  return ndef.NdefRecord(
    typeNameFormat: ndef.TypeNameFormat.media,
    type: Uint8List.fromList(utf8.encode(mimeType)),
    identifier: Uint8List(0),
    payload: Uint8List.fromList(utf8.encode(data)),
  );
}
Enter fullscreen mode Exit fullscreen mode

That's simple to write, but vCards get big fast. A card with name, title, company, phone, email, website and a few social URLs easily passes 144 bytes, so it won't fit an NTAG213. NTAG215 (504 bytes) is the practical minimum. Because each record can be encoded before writing, the app can show the exact size and warn before a write fails halfway.

4. Wi-Fi: TLVs inside a MIME record

Wi-Fi credentials use the Wi-Fi Alliance's Simple Configuration format: a media record of type application/vnd.wfa.wsc whose payload is nested TLVs (2-byte type, 2-byte length, value, all big-endian).

The outer TLV is a Credential (0x100E) containing:

TLV Type Value
Network Index 0x1026 1
SSID 0x1045 network name, UTF-8
Auth Type 0x1003 0x0020 WPA2-Personal, 0x0001 open
Encryption Type 0x100F 0x0008 AES, 0x0002 WEP, 0x0001 none
Network Key 0x1027 password (omitted for open networks)
MAC Address 0x1020 FF FF FF FF FF FF (any)
static List<int> _u16(int v) => [(v >> 8) & 0xff, v & 0xff];
static List<int> _tlv(int type, List<int> value) =>
    [..._u16(type), ..._u16(value.length), ...value];

final credential = [
  ..._tlv(0x1026, [1]),
  ..._tlv(0x1045, utf8.encode(ssid)),
  ..._tlv(0x1003, _u16(0x0020)), // WPA2-Personal
  ..._tlv(0x100F, _u16(0x0008)), // AES
  ..._tlv(0x1027, utf8.encode(password)),
  ..._tlv(0x1020, List.filled(6, 0xff)),
];
final payload = _tlv(0x100E, credential);
Enter fullscreen mode Exit fullscreen mode

Platform note: Android's NFC stack reads this record and offers to join the network. iPhones don't join Wi-Fi from NFC tags, so if your users include iPhone owners, pair the tag with a Wi-Fi QR code.

Count bytes before you write

Every encoder above returns a real NdefRecord, so NdefMessage(records: [...]).byteLength gives the exact size before you touch a tag. Compare that with the tag's capacity (the app reads it during the scan) and you can refuse an oversized write with a clear message, instead of a vague platform error halfway through.

Wrapping up

The NDEF spec is small enough to implement by hand, and doing so pays off. You control every byte, you can test encoders without hardware, and you can tell users exactly what fits on their tag.

I built this for NFC Wizard, an NFC toolkit for iPhone and Android - Coming Soon(disclosure: it's my app). If you're choosing tags for a project, I wrote a plain-language guide to NTAG213 vs NTAG215 vs NTAG216 and one on writing Wi-Fi to a tag.

Top comments (0)