Engineering · Warehouse scanning
How to build a warehouse barcode scanning app for Android rugged scanners
A warehouse scanning app turns a barcode read on a rugged Android terminal into an answer the picker can act on in a glance. We take the scan from the hardware scanner as a broadcast, route every screen through the same scan handler, write status as short field labels with colour for severity, and test every text colour against every background in both the light and the dark theme. Most of our bugs were a fix that existed on one screen and not on the others.
Updated
What a warehouse scanning app has to do
A warehouse scanning app is the program on a rugged Android terminal (a handheld computer with a built-in barcode scanner) that receives each barcode read and shows the picker what to do next: which order this parcel belongs to, whether it is already packed, whether it already left. To build a warehouse scanning app that works, receive the scan from the hardware scanner as an Android broadcast, send every screen through one shared scan path, and never drop a scan silently.
We run an internal scanning app in our own warehouse stations, and we later ported its core into Pick Forge, our public Shopify app for barcode-verified picking. Pick Forge is approved and listed on the Shopify App Store but not yet proven in a real merchant's warehouse. The lessons below come from the internal app.
Step 1: get the scan out of the hardware scanner
On Urovo terminals the settings app calls them Keystroke (the scanner types the code like a keyboard) and Intent (the scanner sends an Android broadcast with the code in an extra). A new or factory-reset terminal comes configured for neither in the way the app expects, and the symptom is always the same: "I scan and nothing happens."
Intent mode is the robust choice. A broadcast reaches the app on any screen, whatever field has focus. The trap is the delivery setting: if delivery is set to Start Activity or Start Service instead of Broadcast, everything looks configured and the app's broadcast receiver receives nothing. Clipboard mode does not work at all.
Keystroke mode needs an Enter suffix. Without a terminator the code is typed into the scan bar and stops there. Keystroke mode also only reaches a focused input field, so we made the base screen collect keys until Enter whenever no text field has focus and hand the code to the same handler that Intent mode uses.
Diagnose from the scan bar, not from the account. If the code appears in the bar and nothing opens, Keystroke mode has no Enter suffix. If nothing appears, Intent mode has the wrong delivery, the terminal is on Clipboard, or the scanner is off. A new colleague once lost almost an hour while we searched the server and his permissions; the difference was the terminal. When something fails for one person only, ask first what the scan bar shows.
Step 2: one scan path for every screen
A scanning app grows several screens that scan: a main screen, batch picking, wave picking, packing. If each screen has its own scan handler, a fix on one screen does not reach the others. Ours had a line on every scan handler that returned early while a previous request was still running, so the next scan was lost in silence. It was fixed on the main screen with a comment describing the exact symptom, "the man scanned 10 parcels and half were not recorded". The batch and packing screens kept the same line, and people later reported it as "loading is slow".
The fix on every screen is a queue plus cache-first display. Scans go into a queue and are processed in order. The order card is shown at once from a local snapshot on the device, and the server reconciles it quietly. Measured over one day, 67.2% of scanned codes were already in the local snapshot. The beep and the warnings ("already packed", "already left") still wait for the server, because the local cache does not know the pick state.
The warehouse had asked for scans to be batched for 5 seconds into a single request. We measured first: a scan cost 47 ms of server time, so batching would have saved about 100 ms in exchange for five seconds of blindness. When you fix a scan behaviour, search for the same line on every screen before you close the task.
Step 3: write for a glance, not for reading
Status text in a scanning app should be a label of the form field: value, not a sentence. A picker reads the screen in a glance with a parcel in hand. "Old label: [number] · cancelled" is seen at once, like a form field; "you are now working on a new label" has to be read and understood, and feels like a chat instead of a tool. The owner of the warehouse rejected every conversational phrasing we tried. Colour carries severity: green for the normal flow, red only for a real problem.
We also removed all emoji and decorative glyphs from the interface, after being asked twice. State and category are plain text, with a coloured dot or a pale tinted pill where it helps. A navigation chevron in a list stays, because it is a standard affordance.
Step 4: a dark theme is a rule about pairs of colours
A dark theme in an Android app works only if every text colour has a dark-theme pair and every background colour has one too. At night our pickers could not read the text because many colours never went through the theme. Colours reached the screen through three channels: theme resources, which switch automatically; colour literals written in code, which never switch; and XML references to colour tokens that had no night variant. We counted 351 literals in code, 169 of them used as text, and 77 of those fell below the WCAG AA contrast level at night. The worst was 1.03:1, effectively invisible.
A small analyser extracts every literal, classifies it as text or background from the line it sits on, and computes WCAG contrast against every background of the theme in both lights. A second script finds the nearest colour that meets a threshold while keeping the hue.
The mirror bug: a text token used as a background. A token that is dark by day and light by night is correct for text and wrong as a fill under fixed white text. We added a neutral fill token that is dark in both themes, and a check that searches for the whole class. When you change what a token means, search for every use of its old role as well: we remapped one token to a fill, and 5 places that used it as text dropped to 2.23:1 within minutes.
User-selectable accent palettes are theme attributes; all 10 combinations of 5 palettes and 2 themes pass 4.5:1 for text and 3:1 for fills. The accent never goes on status text: on the green palette it came out at 1.01:1 against the "all good" green.
Updates and per-store settings
Serve the APK from the web server, not from the application process. Our pickers reported that no update appeared. The update check was timing out. The 29 MB APK was streamed by the single Python process that also answered the tiny version request. Over one day of 235 checks, those near an APK download had a 90th percentile of 14.97 s and 8 failed; those without one took 0.02 s. Served by the web server, the 29 MB took 0.23 s. The update check also had two silent failure paths that looked exactly like "you are up to date"; an explicit check now says when it failed.
Learn the warehouse's words before building on them. When asked to let an admin choose "for which category" product scanning is required, we built on the store platform's product types, 83 of them in one store. In the warehouse, "category" meant the store. The right key for a per-store setting turned out to be the order-number prefix, which we measured as one-to-one with the store across 30 stores over 30 days. The setting is decided on the server in one place and validates the prefix on write, so a typo cannot create a setting that silently matches nothing.
Where we were wrong, and the limits
Our recurring mistake was the same in several forms: a fix that lived on one screen or one code path. We now treat "which other screens do this?" as part of every fix.
Physical stock counted at a station is its own trap. Adding a count on top of the existing pool invents stock; the question to ask first is whether the goods were moved from another location (already in the system) or arrived directly (not in the system). In one real count of 34 SKUs, only 12 were raised. On the Shopify Admin API, inventoryMoveQuantities does not move stock between locations; that takes inventorySetQuantities with one negative and one positive line.
This method does not guarantee that a terminal arrives configured, that a contrast check covers colours you never route through it. Commercial stores also differ from ours: on the first real test of Pick Forge, order numbers were purely numeric and many lines had no SKU, two cases our internal prefixes had hidden.
Checklist
1. Scanner on Intent mode with Broadcast delivery, or Keystroke with an Enter suffix; never Clipboard. 2. One scan path on every screen; no early return that drops a scan. 3. Queue plus cache-first card; warnings only on the server's answer. 4. Status as "field: value", colour for severity, no emoji. 5. Every text and background pair measured in both themes. 6. Large files served by the web server; a failed check never looks like a successful one.
Questions and answers
Why does my Android barcode scanner scan but the app does nothing?
On rugged Android terminals such as Urovo, the scanner sends codes in Keystroke or Intent mode. If the code appears in the app's scan bar but nothing opens, Keystroke mode lacks an Enter suffix. If nothing appears at all, Intent mode is usually set to Start Activity or Start Service delivery instead of Broadcast, the terminal is on Clipboard mode, or the scanner is off. Check the scan bar before checking accounts or the server.
Should a warehouse scanning app use keyboard wedge or broadcast intents?
A warehouse scanning app should prefer broadcast intents, because a broadcast reaches the app on any screen regardless of which field has focus. Keyboard wedge, called Keystroke mode on Urovo terminals, works only with an Enter suffix and only reaches a focused field unless the app collects keys itself until Enter. Supporting both, through one shared scan handler, lets terminals configured either way work.
How do I stop a scanning app from losing scans when the picker scans fast?
A scanning app loses scans when its handler ignores a new scan while the previous request is still running. Put scans into a queue and process them in order, and show the order card immediately from a local snapshot while the server confirms. Keep warnings such as already packed on the server's answer. Apply the same fix to every screen that scans, because each screen often has its own handler.
How do I make a dark theme readable in an Android warehouse app?
Make every colour go through the theme and measure contrast instead of choosing by eye. Colour literals written in code never switch with the theme, and text tokens that are dark by day and light by night become unreadable when used as backgrounds under fixed white text. Measure WCAG contrast for each text and background pair in both lights, and use a separate fill token that stays dark in both themes.
Should a warehouse scanner app use icons and emoji?
In our warehouse, no. Pickers read the screen in a glance, so we show state as short field: value labels in plain text, with a coloured dot or a pale tinted pill where it helps, and colour for severity: green for the normal flow, red only for a real problem. A navigation chevron in a list is kept as a standard affordance, but decorative glyphs and emoji were removed.
How to cite this
Going Stellar (2026). How to build a warehouse barcode scanning app for Android rugged scanners. GOING STELLAR S.R.L..
https://goingstellar.ro/methods/warehouse-scanning-app-android-rugged-scanners/