Building Offline-First Android Apps That Actually Sync
Most apps treat connectivity as a given. Here is how we design data layers for the many places where it isn't.
Most Android apps are built on an assumption that quietly fails: that the network is there. It works fine in the office, on the emulator, and in the demo. Then the app reaches a warehouse basement, a rural clinic, or a train tunnel, and the product falls apart.
Offline-first is not a feature you add later. It is a decision about where truth lives.
Make the local database the source of truth
The single most important change is this: your UI reads from the local database, never from the network.
The network is a background process whose only job is to push local changes up and pull remote changes down. The screen does not know or care whether a sync is in progress.
This inverts the usual flow. Instead of fetch → show → maybe cache, you get write locally → show immediately → sync eventually. The user gets an instant response every time, because nothing on screen is waiting on a radio.
Give every record a sync state
A row in your local database needs more than its business fields. At minimum:
syncStatus— one ofsynced,pending,failedupdatedAt— a local timestampremoteId— null until the server has acknowledged it
That third field matters more than people expect. If a record is created offline, it has no server ID yet. Anything that references it has to reference the local ID and be rewritten when the real one arrives.
Let WorkManager own retries
Do not write your own retry loop. WorkManager already survives process death, respects Doze, and handles exponential backoff:
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30, TimeUnit.SECONDS
)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"sync", ExistingWorkPolicy.KEEP, syncRequest
)
ExistingWorkPolicy.KEEP is the detail that saves you. Without it, an app that enqueues a sync on every screen open will stack up dozens of redundant jobs.
Decide your conflict policy before you need one
Two devices edit the same record offline. Both come back online. What happens?
There are only a few honest answers:
- Last write wins. Simple, and silently destroys data. Acceptable only for genuinely low-stakes fields.
- Field-level merge. Each field carries its own timestamp. More storage, far fewer lost edits.
- Conflict queue. The server detects the collision and a human resolves it.
The wrong answer is not deciding — because then you have picked option one by accident.
For most of the products we build, a conflict queue is worth the extra work. Losing a health worker's patient notes because two phones synced in the wrong order is not a bug you want to explain.
Show sync state honestly
Users tolerate offline. They do not tolerate uncertainty.
A small, persistent indicator — "3 visits waiting to sync" — turns anxiety into information. When something fails permanently, say so, and give them a retry button. A silent failure is the worst outcome of all, because the user believes their work is saved when it is not.
The test that matters
Before release, run this: put the device in airplane mode, complete a full workflow, force-stop the app, restart it, then reconnect.
If everything the user did is still there and arrives on the server, you have built an offline-first app. If not, you have built an online app with a cache.
Keep reading
Cutting Android Cold Start From 9 Seconds to Under One
A practical walkthrough of the profiling and fixes that make the difference on the budget devices your users actually own.
Read articleInsightsWhat to Actually Look For in a Development Partner
Portfolio screenshots tell you very little. These are the questions that separate a studio that ships from one that bills.
Read article