rfid_kit

Zebra RFID readers, Link-OS RFID label printers and DataWedge barcode scanners for Flutter, behind vendor-neutral interfaces.

It was extracted from a warehouse app that runs on Zebra handhelds every day. The code wrapping the Zebra SDKs is the easy part. This package also carries the behaviour that took field failures to get right:

  • The reader is opened once and shared. A Zebra reader takes several seconds to open. Opening and closing it per screen can leave it answering nothing ("Response timeout") until the handheld reboots.
  • After a failed open, the radio is left alone for 30 s. In the field, a failed reader recovered only when nobody touched it.
  • Reading follows the hardware trigger. Hold the trigger to read, release to stop. Stray release events from some handhelds are ignored, and a radio that stops on its own is restarted.
  • The locate meter is built for walking. It filters to the hunted EPCs inside the reader and lowers transmit power while hunting. The meter takes the median over a 1.5 s window and holds through read gaps. It shows "not reading" between trigger pulls instead of decaying.
  • Close-range confirm mode. At arm's length only the right tag still answers, and the neighbouring bin goes silent.
  • Printer connect errors say why. Bluetooth printers are paired automatically, and a failure names the actual cause: Bluetooth off, missing permission, wrong passkey, printer on another subnet, and so on.
  • Crash guard. A crash on a Zebra SDK thread is recorded without killing the app, and handed to you on the next launch.

Supported hardware

Status
Zebra TC22R, built-in RFID reader Field-tested in the original app
Zebra ZT411 printer Field-tested in the original app
DataWedge scan trigger (Zebra TC series) Field-tested in the original app
RFD40 / RFD8500 / RFD90 sleds, MC3300R Wired up through the same Zebra SDK, not field-verified
Other Link-OS ZPL printers Should work, not verified

The package is Android only. USB printing is not implemented.

Installation

flutter pub add rfid_kit

Tested with Flutter 3.35 (AGP 8) and Flutter 3.44 (AGP 9).

Android setup

The Zebra SDKs are proprietary, so this package does not include them. You download them from Zebra and add them to your app.

1. Add the Zebra SDK files

Download these from Zebra's developer portal:

  • RFID SDK for Android: rfidapi3lib-<version>.aar (tested with 2.0.5.275)
  • Link-OS SDK for Android: ZSDK_ANDROID_API.jar, plus the jars shipped next to it (commons-io, commons-lang3, commons-net, commons-validator, core, httpcore, httpmime, jackson-annotations, jackson-core, jackson-databind, opencsv, pkix, prov, snmp6_1z)

Copy all of them into your app's android/app/libs/. If you keep them elsewhere, set rfidKit.zebraLibs=<path relative to android/> in android/gradle.properties. If the files are missing, the build stops and says so.

2. android/app/build.gradle.kts

android {
    defaultConfig {
        // The Zebra RFID SDK requires Android 10 (API 29).
        minSdk = maxOf(flutter.minSdkVersion, 29)
    }

    // The Link-OS jars each carry their own copies of these files.
    packaging {
        resources {
            excludes += setOf(
                "META-INF/DEPENDENCIES",
                "META-INF/NOTICE",
                "META-INF/LICENSE",
                "META-INF/LICENSE.txt",
                "META-INF/NOTICE.txt",
            )
        }
    }
}

dependencies {
    implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar", "*.aar"))))
}

3. android/app/src/main/AndroidManifest.xml

The RFID SDK declares its own app label, so tell the manifest merger to keep yours:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <application
        android:label="My app"
        tools:replace="android:label"
        ...>

The plugin adds the permissions and <queries> entries it needs itself (Bluetooth, DataWedge and the Zebra RFID services). It also adds the R8 keep rules for a minified release.

4. Runtime permissions

A built-in reader needs no runtime permission. A Bluetooth sled or a Bluetooth printer needs BLUETOOTH_SCAN and BLUETOOTH_CONNECT granted before you connect, for example with permission_handler:

await [Permission.bluetoothScan, Permission.bluetoothConnect].request();

Usage

import 'package:rfid_kit/rfid_kit.dart';

Pick mock or real hardware

// auto: real hardware when present, the simulator otherwise (default).
// real: fail loudly if no hardware — use this for the build you ship.
// mock: always the simulator — tests, web, laptops.
DeviceManager.instance.mode = DeviceMode.real;

Open the reader

final session = ReaderSession.instance;
session.notes.listen(print); // open time, power applied, locate reports…

try {
  final reader = await session.reader(); // opened once, then shared
} on DeviceException catch (e) {
  print(e.message); // says what to do, including "wait N seconds"
}

Don't disconnect when a screen closes. Call session.release() only when the app is done with the reader.

Inventory

// Subscribe BEFORE starting, or the first reads are dropped.
final sub = reader.tags.listen((tag) => print('${tag.epc} ${tag.rssi} dBm'));
await reader.startInventory(); // arms the reader; the trigger reads

// later
await reader.stopInventory();
await sub.cancel();

Locate

final meter = reader
    .locate([epc, otherEpcOnTheSamePallet],
        live: session.triggerHeldChanges)
    .listen((value) {
  if (value < 0) {
    // trigger released — not reading
  } else {
    // 0..100, higher is nearer; reader.lastRssi has the dBm
  }
});

await reader.setLocateRange(true);  // close range: confirm the right bin
await meter.cancel();               // ends the locate session
final printer = await DeviceManager.instance.resolvePrinter(
  connection: const ConnectionConfig.tcp('192.168.1.50'), // or .bluetooth(mac)
);
await printer.connect();
await printer.calibrateRfid(); // once per label stock

await printer.printRfidLabel(const RfidLabel(
  barcode: 'SKU-1001',
  epc: 'E28011700000020000000001', // 24 hex chars
  lines: ['Aisle 4', '2026-09-26'],
));

// Your own layout:
await printer.sendRaw('^XA^RFW,H^FD$epc^FS^FO40,40^A0N,40,40^FDHello^FS^XZ');

printRfidLabel refuses a printer that does not speak ZPL. Printing without encoding would give you a sticker that looks right but still carries its factory EPC.

Scan barcodes with the hardware trigger

HardwareScanner.instance.scans.listen((barcode) => print(barcode));

On first use this creates a DataWedge profile for your app. Devices without DataWedge simply never emit, so keep a camera scanner as a fallback. If the grip trigger fires the barcode laser instead of RFID, turn the scanner off for the length of the RFID scan:

await HardwareScanner.instance.setEnabled(false); // ... RFID scan ...
await HardwareScanner.instance.setEnabled(true);

Keep the screen on while scanning

await ScreenWake.keepAwake(true);
// ...
await ScreenWake.keepAwake(false); // always pair it, including on errors

Native crash reports

await RfidDiagnostics.installCrashGuard(); // early, before connecting
final crash = await RfidDiagnostics.takeLastCrash();
if (crash != null) report(crash.trace, fatal: crash.fatal);

Testing your app

MockRfidReader and MockLabelPrinter are exported. MockLabelPrinter records what it was asked to print (printed, raw). With DeviceMode.mock, the whole app runs on a laptop or in a browser.

Adding another reader

Dart only knows the RfidReader and LabelPrinter interfaces. The platform channels are vendor-neutral. To support another reader, register a factory by model:

DeviceManager.instance.registerReader('FXR90', (descriptor, connection) =>
    MyFxr90Reader(descriptor, connection));

On the native side, implement RfidBackend or PrinterBackend and select it in the bridge.

License

MIT for this package. The Zebra SDKs are licensed separately by Zebra.

rfid_kit is not affiliated with or endorsed by Zebra Technologies. Zebra, Link-OS and DataWedge are trademarks of Zebra Technologies.

Libraries

rfid_kit
Zebra RFID readers, Link-OS label printers and DataWedge barcode scanners for Flutter, behind vendor-neutral interfaces.