Standard barcodes require line-of-sight and piece-by-piece scanning—inefficient for mass inventory. RFID lets you read 200 tags in three seconds by just walking along a shelf. We develop mobile apps that ingest a stream of EPC codes without loss, deduplicate them, and reconcile against the expected list—all in real time, even without internet. Our experience: 5 years in mobile development, over 30 projects in logistics and inventory.
For example, in a warehouse with metal shelving, UHF tags often fail to read due to reflections. We tune reader parameters: power, polarization, filters. In one project, we reduced miss rates from 15% to 2% by selecting antennas and configuring settings. This is especially important for metal surfaces where standard settings fail.
Problems We Solve
Duplicate reads—a single tag can be read 50+ times per session. Fast deduplication without UI blocking is needed. Offline mode—warehouses often lack Wi-Fi. The database must be local with later synchronization. Discrepancies—some tags may not be read due to damage or poor placement. We explicitly show found, missing, and extra items. This is critical for warehouses with metal shelves where UHF tags perform worse.
RFID scanning is 100x faster than manual entry or barcodes when checking hundreds of items. But without proper data handling, this advantage is lost.
How to Deduplicate EPC Tags Without Loss
The inventory session is a state machine with transitions: IDLE -> SCANNING -> PROCESSING -> COMPLETED, with pause capability. For each tag read, we update a MutableStateFlow with deduplication by EPC:
class InventorySession(private val expectedItems: List<InventoryItem>) { private val _scannedEpcs = MutableStateFlow<Set<String>>(emptySet()) val scannedEpcs: StateFlow<Set<String>> = _scannedEpcs.asStateFlow() val matchedItems = scannedEpcs.map { epcs -> expectedItems.filter { it.epc in epcs } }.stateIn(scope, SharingStarted.Eagerly, emptyList()) val missingItems = scannedEpcs.map { epcs -> expectedItems.filter { it.epc !in epcs } }.stateIn(scope, SharingStarted.Eagerly, emptyList()) val unexpectedEpcs = scannedEpcs.map { epcs -> val knownEpcs = expectedItems.map { it.epc }.toSet() epcs.filter { it !in knownEpcs } }.stateIn(scope, SharingStarted.Eagerly, emptyList()) fun onTagRead(epc: String) { _scannedEpcs.update { current -> current + epc } } fun reset() { _scannedEpcs.value = emptySet() } } Set<String> provides automatic deduplication. One EPC may arrive 50+ times, but the Set stores it once. Derived states (found, missing, extra) are computed reactively via map.
Why Offline Synchronization Is Critical for Warehouses
We ensure the app works without network. A local Room DB stores the expected list and results:
@Entity(tableName = "inventory_sessions") data class InventorySessionEntity( @PrimaryKey val sessionId: String, val locationId: String, val startedAt: Long, val completedAt: Long?, val status: String // "in_progress", "completed", "synced" ) @Entity(tableName = "scanned_tags") data class ScannedTagEntity( @PrimaryKey val epc: String, val sessionId: String, val firstSeenAt: Long, val readCount: Int ) readCount is the number of reads for a single tag per session. Anomalously low counts (1–2) when neighboring tags were read 20+ times indicate poor physical placement or damage—a useful QA metric.
After session completion, synchronization via WorkManager when network becomes available:
val syncRequest = OneTimeWorkRequestBuilder<InventorySyncWorker>() .setConstraints(Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) .setInputData(workDataOf("session_id" to sessionId)) .build() workManager.enqueueUniqueWork("sync_$sessionId", ExistingWorkPolicy.KEEP, syncRequest) How to Display Results in Real Time
LazyColumn with key(item.epc)—animated addition of found items:
@Composable fun InventoryResultsScreen(session: InventorySession) { val matched by session.matchedItems.collectAsState() val missing by session.missingItems.collectAsState() val scanned by session.scannedEpcs.collectAsState() Column { LinearProgressIndicator( progress = { if (session.expectedItems.isEmpty()) 0f else matched.size.toFloat() / session.expectedItems.size } ) Text("Found: ${matched.size}/${session.expectedItems.size}") LazyColumn { items(matched, key = { it.epc }) { item -> InventoryItemRow(item = item, status = ItemStatus.FOUND) } items(missing, key = { it.epc }) { item -> InventoryItemRow(item = item, status = ItemStatus.MISSING) } } } } GS1 EPC Decoding
EPC is not just a hex string. A structured code like urn:epc:id:sgtin:0614141.107346.2017 contains company, item reference, and serial number. Decoding via SGTIN-96:
SGTIN-96 decoding code example
fun decodeSgtin96(epc: String): Sgtin96? { val bytes = epc.chunked(2).map { it.toInt(16) }.toByteArray() val bits = BigInteger(1, bytes) val header = bits.shiftRight(88).and(BigInteger.valueOf(0xFF)).toInt() if (header != 0x30) return null val filter = bits.shiftRight(85).and(BigInteger.valueOf(0x07)).toInt() val partition = bits.shiftRight(82).and(BigInteger.valueOf(0x07)).toInt() // further parsing per partition table } Ready-made libraries: com.gs4tr.epcis:epcis-rest-client or org.fosstrak.epcis:epcis-repository-client. For more, see GS1 EPC Tag Data Standard.
Comparison of RFID and Barcodes
| Parameter | Barcode | RFID |
|---|---|---|
| Scan speed | 1 item/s | 200 tags in 3 s |
| Line-of-sight required | Yes | No |
| Data rewrite | No | Yes (some tags) |
| Interference resistance | High | Medium (metal, liquid) |
| Tag cost | $0.01 | $0.05–$0.50 |
RFID is 10–100x faster for bulk scanning but requires initial setup and hardware selection.
Comparison of EPC Decoding Methods
| Method | Performance | GS1 Support | Integration Complexity |
|---|---|---|---|
| Manual SGTIN-96 | High (native) | Full | Medium |
| EPCIS library | Medium (HTTP) | Full | Low |
| Cloud service | Low (REST) | Partial | Minimal |
For mobile apps, the first option is optimal—fewest dependencies and maximum speed.
What's Included in the Work
- Architecture design of the inventory state machine
- Development of the deduplication module using
StateFlow - Offline storage implementation with Room
- Synchronization setup via WorkManager
- Integration with BLE reader (Zebra, custom)
- GS1 EPC decoding and validation
- Jetpack Compose UI with animated progress
- Code signing, provisioning, publication to App Store and Google Play
- Documentation and operator training
Evaluate how RFID inventory can reduce your warehouse time—get a consultation from our engineers.
Process
- Analytics—study warehouse processes, tag types, WMS.
- Prototype—MVP in 3–5 days with basic functionality.
- Development—iterative sprints of 2 weeks.
- QA—load testing with real readers.
- Deployment—publish to stores and handover.
Timeline
Mobile inventory app with Zebra/custom BLE reader, offline Room, GS1 decoding, and sync: from 5 days (simple warehouse, one reader, one tag type) to 2–3 weeks (multi-location, multiple tag types, custom EPC scheme, REST integration with WMS).
Contact us for a project estimate—we'll respond within a day. Request a consultation, and we'll show you how to implement RFID inventory in your warehouse. We guarantee compliance with App Store Review Guidelines and Google Play policies, as well as full support at all stages.







