How to Add Offline Mode to a Mobile App
Contents
Offline mode is not a switch you add after launch. It is a decision about what remains trustworthy when the network disappears. A delivery driver may need to complete a stop in a dead zone. A field worker may need today’s assignments on a basement floor. A travel app may need an itinerary without roaming.
The right offline experience is usually smaller than “the whole app works without internet.” Define one important job that still works, make its state clear, and synchronize it safely when connectivity returns.
Decide the exact offline job first, treat the local database as the app’s readable source of truth, queue only the writes that can safely happen offline, make sync state visible, define a conflict rule before launch, and test with real interruptions instead of toggling airplane mode once.
Define what “offline” means for this product
Different features need different levels of offline support.
| Feature | Useful offline behavior | What can wait for a connection? |
|---|---|---|
| Field inspection app | View assigned jobs, capture notes and photos, mark work complete | Uploading photos and syncing completion |
| Task manager | Read existing tasks, create and edit tasks | Sharing a new task with collaborators |
| Travel itinerary | View saved reservations and maps | Refreshing flight or gate changes |
| Store locator | View a previously loaded city or saved locations | Fresh nearby search results |
| Chat app | Read cached conversations and compose a pending message | Delivering the message and loading new history |
Be honest in the interface. “Available offline” should not mean “shows a cached version that may be days old.” Mark when data was last refreshed and state which actions will sync later.
Start with a local source of truth
In an online-only app, a screen often waits for the backend response. In an offline-capable app, the screen reads local data immediately. A sync process refreshes that local data when the network is available.
For an Expo or React Native project, Expo SQLite provides a queryable database that persists across app restarts. It is a better fit for structured offline data—jobs, tasks, messages, forms, and sync records—than using one large JSON blob in device storage.
Keep the device database separate from the backend database. The mobile app needs local records optimized for quick reads and a backend API that remains the authority for shared data and permissions.
Choose the smallest useful local dataset
Do not download a company’s entire database “just in case.” Decide what a user needs before losing connection.
A field-service app might cache:
- The signed-in user’s next 20 assigned jobs.
- Customer names, addresses, contacts, and job instructions.
- The forms required for each job.
- Reference photos already opened or marked for download.
- The user’s own unsent completion records.
It probably should not cache every customer, every old job, or staff-only financial information.
Define each local collection with four rules:
| Question | Example answer |
|---|---|
| Who may store it? | The currently authenticated assigned worker |
| How long may it remain? | Seven days or until job completion |
| How does it refresh? | On app launch, pull-to-refresh, and reconnect |
| What happens after sign-out? | Remove private local records and pending work safely |
Avoid treating a cache as secure storage. Apply encryption and device-level protections where appropriate, minimize sensitive data, and clear it when the authenticated context changes.
Separate reads from writes
Offline reads and offline writes are different problems.
A cached read can be stale but still useful. An offline write can create a disagreement with the backend or another device. Android’s offline-first architecture guidance describes a practical pattern: read from the local data source, then synchronize with the network; for important offline writes, save locally first and queue the network update.
Classify each write before you allow it offline.
| Write type | Offline approach |
|---|---|
| Draft note | Save locally and sync later |
| Completed inspection form | Save locally, queue with attachments, show pending state |
| “Like” or analytics event | Queue only if losing it is acceptable |
| Card payment | Usually require an online confirmation path |
| Appointment time change shared with others | Require online validation or make it a local draft |
| Inventory count | Queue only with a clear conflict/approval rule |
An offline-first app does not need offline writes everywhere. Refusing a risky action with a clear explanation is better than pretending it succeeded.
Model pending work explicitly
Do not hide unsent work in a background promise. Store it as a durable record.
A pending-operation table can include:
| Field | Purpose |
|---|---|
id | Unique local operation id |
type | Create task, update form, upload image, and so on |
entity_id | Local or server record it affects |
payload | Minimal data needed to replay the request |
idempotency_key | Prevents duplicates if the request is retried |
created_at | Preserves order and support context |
attempt_count | Enables bounded retries |
last_error | Gives the user and support team a real explanation |
status | Pending, syncing, failed, or completed |
The visible state should match the local record:
A person should never need to guess whether “Save” reached the team, customer, or backend.
Build a deliberate sync flow
Sync is not “try every request again on reconnect.” It is a controlled sequence.
- Confirm the user still has a valid session.
- Refresh the server’s changes that affect the local scope.
- Send queued writes in a safe order.
- Handle server acknowledgements, validation failures, or conflicts.
- Refresh records changed by accepted writes.
- Update local sync status and show any action the user must take.
Use idempotency keys for creates or important updates. If the device sends the same completion record twice after a timeout, the backend should return the already-created result rather than creating a duplicate job completion.
Retry temporary network failures with backoff. Do not retry a permanent failure—such as expired permission, invalid form data, or deleted assignment—until the user reviews it. A growing silent retry loop drains battery and hides the real issue.
Choose a conflict rule before users create conflicts
Conflicts appear when more than one device changes the same shared record, or when a server update arrives after an offline edit. There is no universal solution.
| Conflict type | Usually sensible rule |
|---|---|
| Personal draft only one user owns | Last local edit can win |
| Shared task title | Show both versions or use a clear last-write rule |
| Inventory quantity | Server-side transaction or manual resolution |
| Appointment slot | Backend validates availability; local app shows a failed pending action if taken |
| Inspection form after supervisor edits template | Preserve submitted answers; require review only for changed mandatory fields |
| Deleted server record | Do not recreate automatically; show an unavailable state |
Version fields, server timestamps, and a record’s last sync version make reconciliation possible. Android’s guidance notes that conflict resolution requires bookkeeping and that “last write wins” is one common strategy—but it is only safe when losing the earlier value is acceptable.
For any financial, regulated, inventory, or scheduling action, decide whether the backend must approve the action while online. Offline convenience is not worth corrupting the authoritative record.
Design offline states into the interface
Connectivity should influence the screen without taking over the screen.
Good patterns include:
- A small “Offline” or “Last updated 14 min ago” indicator near affected data.
- A clear “Saved on this device” state immediately after a local action.
- A compact sync queue or error list for people who need accountability.
- A retry action for a failed item, not one giant opaque “Sync again” button.
- Cached content that remains readable while a refresh is unavailable.
- A reason when an action requires connectivity, before the user spends time filling it out.
Avoid blocking alerts for every temporary network change. A person can keep reading and working while the app quietly returns online.
Handle attachments separately
Photos, signatures, videos, and PDFs complicate offline work because the local file and the server record may arrive at different times.
Use a two-step model:
- Save the file locally and attach it to a pending operation.
- Upload the file when online, verify it succeeded, then finalize the related backend record.
Do not mark an inspection complete on the server until required attachments are present. If an image upload fails after the form is saved locally, keep both the image and its pending record so the user can retry or remove it intentionally.
See how to add image and file uploads to a mobile app for the storage, ownership, and retry side of that flow.
Protect the backend during sync
The backend still enforces authorization when the device reconnects. A locally cached assignment does not prove the user may still complete it.
For every queued write:
- Derive identity from the current authenticated session.
- Verify the user still owns or is assigned to the record.
- Validate the payload again on the server.
- Reject stale, duplicate, or unauthorized writes clearly.
- Return enough information for the device to reconcile its local state.
Never trust a local user_id, assignment status, or price merely because it was valid when the device went offline.
Test the uncomfortable cases
Offline capability is proven by interruption, not a happy-path demo.
Test on real iOS and Android devices:
| Test | Expected result |
|---|---|
| App opens with no network | Cached core data appears with a clear freshness state |
| User saves a permitted offline action | A durable pending record appears immediately |
| App is killed before sync | Pending work survives restart |
| Wi-Fi changes to cellular mid-upload | Upload resumes or shows a useful retry state |
| Same action retries after a timeout | Backend creates one result |
| Another device changed the record | App applies the written conflict rule |
| User loses access while offline | Backend rejects the queued operation safely |
| Storage is nearly full | App prevents or explains a risky download/capture |
| User signs out | Private cached data and tokens follow your retention rule |
| Sync fails repeatedly | The user sees which item needs attention and why |
Use release-like builds and real accounts. Emulator network toggles are useful, but they do not cover app termination, device storage, mobile radio changes, or background behavior.
Decide whether offline mode is worth the complexity
Offline mode is worth building when a person loses a core workflow without it and the product can define safe local behavior. It is not automatically valuable for every dashboard, social feed, or search experience.
Start with one high-value offline journey. For example: “A technician can open assigned work, complete a checklist, capture photos, and see exactly what will sync later.” Once that works reliably, add the next journey.
Build an offline-capable app with Huxly
Huxly can help you build the local screens, structured device storage, authenticated backend, pending-operation states, and real-device test flow for an offline-capable Expo, Flutter, or SwiftUI app. The essential product decision is still yours: define what must work without a connection, then build only the local data and sync logic that support it.
FAQ
Does offline-first mean every feature works without internet?
No. It means the app has a deliberate, useful offline behavior for the features that matter. Some actions—payments, live availability, or sensitive approvals—may correctly require a connection.
Should local data or the backend be the source of truth?
The local database should be the immediate source for what the mobile interface displays. The backend remains the authority for shared records, permissions, and conflict resolution.
How do I prevent duplicate offline submissions?
Give each important action an idempotency key, store it in the pending-operation record, and make the backend return the original result if the same key is received again.
What is the easiest conflict strategy?
Last-write-wins can be acceptable for personal notes or other low-risk edits. It is not safe for appointments, inventory, payments, or data where losing another person’s update matters.
Can I store photos offline?
Yes, but treat them as part of an explicit pending upload. Keep the local file, link it to the parent action, upload it when online, and show a clear state until the backend confirms it.
How should the app look when it is offline?
Keep it useful. Show cached data, the last refresh time, pending work, and a clear explanation only where an action requires connectivity. Do not turn normal connectivity changes into repeated blocking alerts.
Conclusion
Offline mode earns trust when it makes one real job possible without hiding uncertainty. Use a local source of truth for readable data, record pending work durably, and let the backend reconcile shared or risky operations once a connection returns.
The most reliable first version is not the one that promises everything offline. It is the one that handles one valuable journey, its attachments, sync failures, and conflicts honestly.
