zebra_rfid_plus 0.0.2
zebra_rfid_plus: ^0.0.2 copied to clipboard
A Flutter plugin for Zebra RFID readers (RFD8500, RFD40, RFD90) using the Zebra RFID API3 SDK. Supports Bluetooth and USB transport, inventory scanning, tag access, pre-filters, and singulation control.
import 'package:flutter/material.dart';
import 'package:zebra_rfid_plus/zebra_rfid_plus.dart';
void main() => runApp(const ZebraRfidExample());
class ZebraRfidExample extends StatelessWidget {
const ZebraRfidExample({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Zebra RFID Example',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: Colors.deepPurple,
useMaterial3: true,
),
home: const RfidHomePage(),
);
}
}
class RfidHomePage extends StatefulWidget {
const RfidHomePage({super.key});
@override
State<RfidHomePage> createState() => _RfidHomePageState();
}
class _RfidHomePageState extends State<RfidHomePage> {
// ─── Controller ───
final _controller = ZebraRfidController();
// Barcode results (not in controller, listen directly)
final List<String> _barcodes = [];
@override
void initState() {
super.initState();
_controller.init();
_controller.addListener(_rebuild);
// Link trigger button → auto start/stop inventory
_controller.linkTriggerToInventory(enabled: true);
// Listen for barcodes separately
ZebraRfid.onBarcodeRead.listen((code) {
if (!mounted) return;
setState(() => _barcodes.insert(0, code));
});
}
void _rebuild() {
if (mounted) setState(() {});
}
@override
void dispose() {
_controller.removeListener(_rebuild);
_controller.dispose();
super.dispose();
}
// ─── Actions ───
Future<void> _toggleConnection() async {
try {
if (_controller.isConnected) {
await _controller.disconnect();
} else {
await _controller.connect(transport: RfidTransport.bluetooth);
}
} on ZebraRfidException catch (e) {
_showError(e.message);
}
}
Future<void> _toggleScan() async {
try {
if (_controller.isScanning) {
await _controller.stopInventory();
} else {
await _controller.startInventory();
}
} on ZebraRfidException catch (e) {
_showError(e.message);
}
}
void _showError(String msg) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(msg), backgroundColor: Colors.red.shade700),
);
}
// ─── UI ───
@override
Widget build(BuildContext context) {
final cs = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Zebra RFID'),
centerTitle: false,
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: _StatusDot(connected: _controller.isConnected),
),
],
),
body: Column(
children: [
_ConnectionCard(
controller: _controller,
onConnectTap: _toggleConnection,
),
const Divider(height: 1),
_ScanBar(
controller: _controller,
onScanTap: _toggleScan,
),
const Divider(height: 1),
Expanded(
child: _TagList(tags: _controller.tags),
),
],
),
);
}
}
// ─────────────────────────────────────────────
// Sub-widgets
// ─────────────────────────────────────────────
class _StatusDot extends StatelessWidget {
final bool connected;
const _StatusDot({required this.connected});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 10,
height: 10,
decoration: BoxDecoration(
color: connected ? Colors.green : Colors.grey,
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
Text(
connected ? 'Connected' : 'Disconnected',
style: Theme.of(context).textTheme.labelSmall,
),
],
);
}
}
class _ConnectionCard extends StatelessWidget {
final ZebraRfidController controller;
final VoidCallback onConnectTap;
const _ConnectionCard({
required this.controller,
required this.onConnectTap,
});
@override
Widget build(BuildContext context) {
final status = controller.connectionStatus;
final readerName = controller.connectedReaderName;
final lastError = controller.lastError;
final isConnecting = status == RfidConnectionStatus.connecting;
return Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
readerName ?? 'No reader connected',
style: Theme.of(context).textTheme.titleSmall,
overflow: TextOverflow.ellipsis,
),
if (lastError != null)
Text(
lastError,
style: TextStyle(
color: Colors.red.shade700,
fontSize: 12,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)
else
Text(
status.name,
style: TextStyle(
color: Colors.grey.shade600,
fontSize: 12,
),
),
],
),
),
const SizedBox(width: 12),
FilledButton(
onPressed: isConnecting ? null : onConnectTap,
style: FilledButton.styleFrom(
backgroundColor: controller.isConnected
? Colors.red.shade600
: null,
),
child: isConnecting
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
)
: Text(controller.isConnected ? 'Disconnect' : 'Connect'),
),
],
),
);
}
}
class _ScanBar extends StatelessWidget {
final ZebraRfidController controller;
final VoidCallback onScanTap;
const _ScanBar({required this.controller, required this.onScanTap});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'${controller.tagCount} unique tags',
style: Theme.of(context).textTheme.titleSmall,
),
Text(
controller.isScanning ? 'Scanning…' : 'Idle',
style: TextStyle(
fontSize: 12,
color: controller.isScanning
? Theme.of(context).colorScheme.primary
: Colors.grey.shade600,
),
),
],
),
),
const SizedBox(width: 12),
FilledButton.icon(
onPressed: controller.isConnected ? onScanTap : null,
icon: Icon(
controller.isScanning ? Icons.stop_rounded : Icons.wifi_tethering,
size: 18,
),
label: Text(controller.isScanning ? 'Stop' : 'Scan'),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: controller.tagCount > 0 ? controller.clearTags : null,
child: const Text('Clear'),
),
],
),
);
}
}
class _TagList extends StatelessWidget {
final List<RfidTag> tags;
const _TagList({required this.tags});
@override
Widget build(BuildContext context) {
if (tags.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.nfc, size: 52, color: Colors.grey.shade300),
const SizedBox(height: 12),
Text(
'No tags yet',
style: TextStyle(color: Colors.grey.shade500),
),
const SizedBox(height: 4),
Text(
'Connect a reader and start scanning',
style: TextStyle(color: Colors.grey.shade400, fontSize: 12),
),
],
),
);
}
return ListView.separated(
padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: tags.length,
separatorBuilder: (_, __) => const Divider(height: 1, indent: 56),
itemBuilder: (context, index) {
final tag = tags[index];
return ListTile(
dense: true,
leading: CircleAvatar(
radius: 18,
backgroundColor: _rssiColor(tag.peakRssi).withOpacity(0.15),
child: Icon(
Icons.nfc,
size: 16,
color: _rssiColor(tag.peakRssi),
),
),
title: Text(
tag.tagId,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
trailing: _RssiChip(rssi: tag.peakRssi),
);
},
);
}
Color _rssiColor(int rssi) {
if (rssi > -50) return Colors.green;
if (rssi > -70) return Colors.orange;
return Colors.red;
}
}
class _RssiChip extends StatelessWidget {
final int rssi;
const _RssiChip({required this.rssi});
Color get _color {
if (rssi > -50) return Colors.green;
if (rssi > -70) return Colors.orange;
return Colors.red;
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: _color.withOpacity(0.12),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _color.withOpacity(0.4), width: 0.8),
),
child: Text(
'$rssi dBm',
style: TextStyle(
color: _color,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
);
}
}