OneTill is a free point-of-sale and inventory app for Android, made for shops where the owner is also the cashier. It runs entirely on the phone, works without internet, and needs no account. It is written in Kotlin with Jetpack Compose, Room, CameraX and ML Kit.
This post covers the decisions that shaped it and the three problems that cost the most time: money maths, a printer that ignored every text command, and a billing bug that looked like someone else's fault.
Start with the shopkeeper
The first design question was who stands behind the counter. For a corner shop or a market stall it is one person, often with a customer waiting and one hand full. That ruled out a few things early.
There is no OneTill account and no server. The app asks for a PIN and keeps everything in a local database. We looked at syncing between two phones and dropped it for the first release: merging stock counts needs per-row IDs, a change log and conflict rules, and a wrong merge means a wrong count on the shelf. One phone, one source of truth.
Money is integers
Prices are stored as whole cents, so $12.50 is stored as 1250. Quantities are stored in thousandths, so a half kilo of cheese is 500. Tax rates are stored in basis points, so 8.25% is 825.
We did this because floating point and refunds do not mix. A partial return has to reverse the right share of the discount and the tax, and the rounding has to land on exactly the same total the customer paid. With integers that becomes ordinary arithmetic you can unit test, and we do.
Every action that touches money is one Room transaction. A sale writes the sale, its lines and the stock movements together. If anything fails, nothing is saved, and the shelf count never disagrees with the till.
Scanning
Barcode scanning uses CameraX with ML Kit. The scanner is full screen with an aiming frame, a torch button and a running count and total, so you can scan a whole basket and then tap Done to return to the order. A code that stays in view is added once, not once per frame. It also accepts keyboard-style scanners.
The printer that printed nothing
Receipt printing was meant to be the easy part. The standard approach for cheap 58 mm thermal printers is ESC/POS: you send text and a few control codes. The printer on our test bench, a USB model, ignored all of it.
It turned out to be a label printer that speaks TSPL and has no built-in fonts at all. There was nothing for text commands to select. The fix was to render the receipt as a bitmap, 384 dots wide (58 mm at 8 dots per millimetre), and send the image. We got the exact command bytes by capturing what the Windows driver sent when it printed a test label.
ESC/POS stays for normal receipt printers, and the setting is called printer type: ESC/POS or picture. If you are debugging a cheap printer that prints blanks, check whether it is a label printer before you blame your commands.
Backup without accounts
With no OneTill account there is no cloud copy of your data, so backup had to be simple and the shopkeeper's own. You can export a file (.otb, with an optional passphrase) and restore it on another phone. Google Drive backup goes into the app's private folder in your own Drive, keeps the latest five, and runs daily for Pro users. If someone forgets their PIN, the phone's screen lock is the recovery route.
A billing bug that looked like Google's
OneTill Pro is one subscription that removes ads and adds a few extras. For days the Pro screen showed "Plans unavailable" and we assumed Play was slow. That was partly true. New subscriptions can take a while to show up, and our testing track had been paused with a change waiting for review, which made Play return "Item not found".
But there was also a bug of ours underneath. We had enabled automatic service reconnection in the billing client, which makes the client report itself as ready at startup. Our start function checked that flag and returned early, so the product query never ran. The fix was to load products whenever the client is ready and to refresh them when the Pro screen opens. We found it by logging the result of the product query, and we should have added that log on day one.
Testing without losing a shop's data
Room schema changes need a migration, and the only reliable test is installing the new version over data from the old one. We have migration tests for every version and an on-device suite of about 100 tests. The suite clears app data, so it runs on emulators only, never on a phone that holds real records.
What is next
Bluetooth printing is not built yet. Most of the remaining work is polish from real use: fewer taps on the sell screen and clearer reports.
OneTill is free, with ads. An optional Pro subscription removes them.
- Google Play: OneTill on Google Play
- Details and screenshots: orionapps.com/onetill
If you build for small shops, or you have fixed a printer that would not print, I would like to hear how you did it.