Topics covered * Cache strategy * Stale-data indication * Synchronization * Conflict resolution * Source of truth
Android Offline Support
Offline support allows an application to continue working when the internet is unavailable or unstable.
The application stores data locally, displays it immediately and synchronizes it with the server when connectivity returns.
1. Offline Support Flow
flowchart TD
A["UI requests data"] --> B["Read local database"]
B --> C["Display cached data"]
C --> D{"Internet available?"}
D -- Yes --> E["Fetch server data"]
E --> F["Update local database"]
F --> G["UI observes updated data"]
D -- No --> H["Continue with cached data"]The UI should normally observe the local database. Network responses first update the database, and the database automatically updates the UI.
2. Cache Strategy
What is a cache?
A cache is a temporary local copy of data received from the server.
In Android, cached data can be stored using:
- Room database
- DataStore
- Files
- In-memory objects
- HTTP cache through OkHttp
Why do we need caching?
Caching provides:
- Faster screen loading
- Reduced API calls
- Lower data usage
- Better experience on slow networks
- Limited offline functionality
Common cache strategies
2.1 Cache First
The application checks the local cache first. It requests the server only if cached data is unavailable or expired.
flowchart TD
A["Request data"] --> B{"Valid cache available?"}
B -- Yes --> C["Return cached data"]
B -- No --> D["Fetch from server"]
D --> E["Update cache"]
E --> F["Return new data"]Suitable for:
- Product categories
- User preferences
- Configuration data
- Content that rarely changes
Advantage: Fast and reduces network calls.
Limitation: The user may receive older data.
2.2 Network First
The application requests fresh data from the server. If the request fails, it uses cached data.
flowchart TD
A["Request data"] --> B["Fetch from server"]
B --> C{"Request successful?"}
C -- Yes --> D["Update cache"]
D --> E["Return fresh data"]
C -- No --> F{"Cache available?"}
F -- Yes --> G["Return cached data"]
F -- No --> H["Show error"]Suitable for:
- Account details
- Latest prices
- Transaction history
- Frequently changing data
Advantage: Normally provides fresh information.
Limitation: Initial loading depends on network speed.
2.3 Cache and Network
The application immediately displays cached data and then requests fresh data from the server.
flowchart TD
A["Open screen"] --> B["Display cached data"]
B --> C["Fetch latest server data"]
C --> D["Update local database"]
D --> E["Refresh UI automatically"]This is also called stale-while-revalidate.
Suitable for:
- News feeds
- Product lists
- User profiles
- Account dashboards
Advantage: Fast initial loading with automatic refresh.
Limitation: The data may change shortly after appearing.
2.4 Network Only
Data is always fetched from the server and is not cached.
Suitable for:
- OTP generation
- Final payment submission
- Highly sensitive one-time operations
Limitation: The feature does not work without connectivity.
2.5 Cache Only
Data is read only from local storage.
Suitable for:
- Downloaded documents
- Saved drafts
- Offline settings
- Previously downloaded media
Cache strategy comparison
| Strategy | First source | Fallback | Best use |
|---|---|---|---|
| Cache first | Local cache | Network | Data that changes slowly |
| Network first | Server | Local cache | Fresh data is important |
| Cache and network | Cache, then server | Cached data | Fast and responsive screens |
| Network only | Server | Usually error | Real-time sensitive operations |
| Cache only | Local storage | Usually error | Downloaded or offline content |
3. Stale-Data Indication
What is stale data?
Stale data is cached data that may no longer match the latest server data.
For example:
Data cached at 10:00 AM
User opens app at 2:00 PM
Network is unavailable
Application displays 10:00 AM dataThe data is useful, but it may not be current.
Why should stale data be indicated?
Without an indication, the user may think old data is current. This can be dangerous in banking, payment, inventory and healthcare applications.
Recommended UI indications
The application can display:
- “Last updated 2 hours ago” “You are viewing offline data”
- “Data may be outdated”
- Offline icon or banner
- Refresh button
- Pending synchronization status
Example:
Account balance: ₹25,000
Offline data
Last updated: 10:30 AMData model example
data class AccountUiState(
val accounts: List<Account> = emptyList(),
val isLoading: Boolean = false,
val isOffline: Boolean = false,
val lastUpdatedAt: Long? = null,
val errorMessage: String? = null
)A database entity can store the update time:
@Entity(tableName = "accounts")
data class AccountEntity(
@PrimaryKey val accountId: String,
val accountName: String,
val balance: Double,
val lastUpdatedAt: Long
)Checking whether data is stale
fun isStale(
lastUpdatedAt: Long,
currentTime: Long
): Boolean {
val cacheDuration = 30 * 60 * 1000L // 30 minutes
return currentTime - lastUpdatedAt > cacheDuration
}The accepted age depends on the type of data:
| Data | Possible cache validity |
|---|---|
| Static configuration | Several days |
| Product catalogue | Several hours |
| News feed | A few minutes |
| Account balance | Very short duration |
| Payment confirmation | Must be server-confirmed |
These durations are business decisions, not fixed Android rules.
4. Synchronization
What is synchronization?
Synchronization keeps the local database and remote server consistent.
It can happen in two directions:
Server → Local databaseThis downloads new or updated server data.
Local database → ServerThis uploads offline changes made by the user.
Synchronization flow
flowchart TD
A["User changes data offline"] --> B["Save change locally"]
B --> C["Mark record as pending"]
C --> D{"Network available?"}
D -- No --> E["Keep operation in queue"]
E --> D
D -- Yes --> F["Send change to server"]
F --> G{"Request successful?"}
G -- Yes --> H["Mark record as synchronized"]
G -- No --> I["Retry or show failure"]Example: Creating a note offline
When the user creates a note without internet:
1. Generate a local ID.
2. Save the note in Room.
3. Mark its sync status as PENDING.
4. Display it immediately.
5. Schedule synchronization.
6. Upload it when the required conditions are met.
7. Save the server ID and mark it as SYNCED.
Sync-status model
enum class SyncStatus {
SYNCED,
PENDING_CREATE,
PENDING_UPDATE,
PENDING_DELETE,
SYNC_FAILED
}@Entity(tableName = "notes")
data class NoteEntity(
@PrimaryKey val localId: String,
val serverId: String?,
val title: String,
val description: String,
val updatedAt: Long,
val syncStatus: SyncStatus
)Repository example
class NoteRepository(
private val noteDao: NoteDao,
private val api: NoteApi
) {
fun observeNotes(): Flow<List<NoteEntity>> {
return noteDao.observeNotes()
}
suspend fun createNote(title: String, description: String) {
val note = NoteEntity(
localId = UUID.randomUUID().toString(),
serverId = null,
title = title,
description = description,
updatedAt = System.currentTimeMillis(),
syncStatus = SyncStatus.PENDING_CREATE
)
noteDao.insert(note)
}
suspend fun synchronizePendingNotes() {
val pendingNotes = noteDao.getPendingNotes()
pendingNotes.forEach { note ->
try {
val response = api.createNote(
title = note.title,
description = note.description
)
noteDao.markAsSynced(
localId = note.localId,
serverId = response.id
)
} catch (exception: Exception) {
noteDao.markAsFailed(note.localId)
}
}
}
}Synchronization using WorkManager
WorkManager is suitable for synchronization that:
- Must eventually complete
- Can run in the background
- Requires internet connectivity
- Should be retried after temporary failure
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.setConstraints(constraints)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
30,
TimeUnit.SECONDS
)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"offline_data_sync",
ExistingWorkPolicy.KEEP,
syncRequest
)The worker performs the actual synchronization:
class SyncWorker(
appContext: Context,
workerParameters: WorkerParameters,
private val repository: NoteRepository
) : CoroutineWorker(appContext, workerParameters) {
override suspend fun doWork(): Result {
return try {
repository.synchronizePendingNotes()
Result.success()
} catch (exception: IOException) {
Result.retry()
} catch (exception: Exception) {
Result.failure()
}
}
}Synchronization considerations
A good synchronization system should handle:
- Retry with exponential backoff
- Duplicate requests
- Partial failure
- Process death
- Device restart
- Multiple queued changes
- Deleted records
- Server validation errors
- Authentication expiry
-
5. Conflict Resolution
What is a synchronization conflict?
A conflict occurs when the same data is changed locally and remotely before synchronization completes.
Example
10:00 AM: Device downloads customer address.
10:05 AM: User changes address offline on mobile.
10:06 AM: Address is changed on the website.
10:10 AM: Mobile reconnects and tries to upload its change.The server and mobile database now contain different updates.
Conflict-resolution strategies
5.1 Server Wins
The server version replaces the local version.
Local change + Server change → Keep server changeSuitable for:
- Account balance
- Payment status
- Server-controlled permissions
- Inventory calculated by the server
Advantage: Centralized and secure.
Limitation: The user’s offline change may be lost.
5.2 Client Wins
The local version overwrites the server version.
Local change + Server change → Keep local changeSuitable for:
- Personal drafts
- Device-specific preferences
- User-controlled content
Risk: It may overwrite a more recent server change.
5.3 Last Write Wins
The update with the latest timestamp is accepted.
Local updatedAt > Server updatedAt → Local wins
Server updatedAt > Local updatedAt → Server winsAdvantage: Easy to implement.
Limitations:
- Device clocks may be incorrect.
- A newer timestamp does not always mean the correct update.
- One update can silently overwrite another.
Prefer server-generated timestamps or versions.
5.4 Version-Based Resolution
Every record contains a version number.
Initial server version = 5
Mobile edits version 5
Website changes version 5 → server version becomes 6
Mobile uploads update based on version 5
Server detects mismatch → conflictRequest example:
{
"id": "123",
"name": "Mahesh",
"version": 5
}Server response:
{
"error": "VERSION_CONFLICT",
"currentVersion": 6
}The application can then reload the latest data or ask the user to resolve it.
5.5 Field-Level Merge
Changes to different fields are combined.
Mobile changes phone number
Website changes email address
Result:
New phone number + new email addressThis is more user-friendly but requires additional business logic.
5.6 Manual Resolution
The application displays both versions and asks the user to choose.
Your version: Server version:
Address A Address B
[Keep Mine] [Use Server Version]Suitable for:
- Important documents
- Collaborative content
- Complex forms
- Changes that must not be silently lost
Strategy comparison
| Strategy | Resolution | Suitable for |
|---|---|---|
| Server wins | Server data replaces local data | Balance, payment and permissions |
| Client wins | Local data replaces server data | Personal drafts and preferences |
| Last write wins | Latest update is selected | Simple, low-risk records |
| Version based | Version mismatch detects conflict | Business and enterprise applications |
| Field-level merge | Independent fields are combined | Profiles and editable forms |
| Manual resolution | User selects the correct version | Important or complex data |
Conflict resolution is primarily a business decision. No single strategy is suitable for every type of data.
6. Source of Truth
What is a source of truth?
The source of truth is the location whose data the application treats as the current authoritative state.
In an offline-first Android screen, the local database is commonly the UI’s source of truth.
flowchart LR
A["Remote API"] --> B["Repository"]
B --> C["Room database"]
C --> D["ViewModel"]
D --> E["UI"]The API response should update Room instead of being sent directly to the UI.
Why use Room as the UI source of truth?
- UI can work without internet.
- Data remains available after process death.
- UI observes a single data source.
- Cache and network results do not compete.
- Updates are automatically emitted through
Flow. - Architecture becomes easier to test.
Repository example
interface AccountRepository {
fun observeAccounts(): Flow<List<Account>>
suspend fun refreshAccounts()
}class AccountRepositoryImpl(
private val api: AccountApi,
private val accountDao: AccountDao
) : AccountRepository {
override fun observeAccounts(): Flow<List<Account>> {
return accountDao.observeAccounts()
.map { entities ->
entities.map { it.toDomain() }
}
}
override suspend fun refreshAccounts() {
val remoteAccounts = api.getAccounts()
accountDao.replaceAll(
remoteAccounts.map { it.toEntity() }
)
}
}The ViewModel observes the database:
class AccountViewModel(
private val repository: AccountRepository
) : ViewModel() {
val accounts = repository.observeAccounts()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = emptyList()
)
fun refresh() {
viewModelScope.launch {
repository.refreshAccounts()
}
}
}Complete data flow
flowchart TD
A["UI observes state"] --> B["ViewModel"]
B --> C["Repository"]
C --> D["Room database"]
D --> C
C --> B
B --> A
C --> E["Remote API"]
E --> C
C --> DRead flow
Room → Repository → ViewModel → UIRefresh flow
API → Repository → Room → ViewModel → UIOffline-write flow
UI → ViewModel → Repository → Room
↓
Pending sync
↓
API when online7. Complete Offline-First Architecture
flowchart TD
U["UI"] --> V["ViewModel"]
V --> R["Repository"]
R --> DB["Room: source of truth"]
DB --> R
R --> V
V --> U
R --> API["Remote API"]
API --> R
R --> Q["Pending operations"]
Q --> W["WorkManager sync"]
W --> APIResponsibilities
| Component | Responsibility |
|---|---|
| UI | Displays data, offline status and sync status |
| ViewModel | Manages UI state and user actions |
| Repository | Coordinates local and remote data |
| Room | Provides persistent data to the UI |
| API | Provides remote authoritative data |
| WorkManager | Executes reliable deferred synchronization |
| Conflict resolver | Decides which version should be stored |
8. Banking Application Example
Offline support must be carefully controlled in banking applications.
Information that may be cached
- Previously fetched transaction history
- Beneficiary list
- Branch information
- Help content
- User preferences
Operations that may be prepared offline
- Form drafts
- Support-message drafts
- Non-sensitive preferences
Operations that normally require a server
- Balance confirmation
- Money transfer
- OTP validation
- Login authentication
- Beneficiary activation
- Payment-status confirmation
Example UI:
Available balance: ₹25,000
Last updated: 10:30 AM
Offline data—refresh before making a payment.Never treat a cached balance as final confirmation of available funds.
9. Common Mistakes
1. Sending API data directly to the UI
API → UI
Room → UIThis creates multiple competing data sources.
Use:
API → Room → UI2. Hiding stale-data status
Users may make decisions based on outdated information. Display the last update time and offline status.
3. Relying only on connectivity callbacks
A device may be connected to Wi-Fi but still have no working internet. Execute the request and handle the actual result.
4. Retrying every error
Retry temporary failures such as:
- Timeout
- Temporary server error
- Network unavailable
Do not repeatedly retry permanent failures such as:
- Invalid request
- Permission denied
- Business validation failure
5. Ignoring duplicate operations
A retried payment or order request can create duplicate transactions.
Use:
- Idempotency keys
- Unique operation IDs
- Server-side duplicate detection
6. Deleting local data before server confirmation
For offline deletion:
1. Mark the item as pending deletion.
2. Hide it from the normal UI if required.
3. Send deletion to the server.
4. Permanently delete it locally after confirmation.
7. Using timestamps without considering clock differences
The device clock may be incorrect. Prefer server timestamps or record-version numbers for conflict detection.
8. Showing every synchronization as a blocking loader
If cached data exists, display it immediately. Perform refresh or synchronization in the background and show a small status indicator.
---
10. Interview Questions and Answers
1. What is offline-first architecture?
It is an architecture where the application primarily reads and writes local data, while synchronization with the server happens separately.
2. What should be the source of truth in an offline-first Android screen?
The local Room database is commonly the UI’s source of truth. Network responses update Room, and the UI observes Room.
3. What is stale data?
Stale data is cached data that may no longer match the latest server state.
4. How do you inform users that cached data may be old?
Display an offline indicator, last-updated time, pending-sync status and a refresh option.
5. How do you synchronize offline changes?
Save them locally with a pending status and use a reliable background job, commonly WorkManager, to upload them when connectivity becomes available.
6. What is a synchronization conflict?
It occurs when the same record is changed locally and remotely before the changes are synchronized.
7. What are common conflict-resolution strategies?
Server wins, client wins, last write wins, version-based detection, field-level merge and manual resolution.
8. Which conflict strategy is best?
There is no universal strategy. The correct strategy depends on the data and business requirements.
9. Why is WorkManager suitable for synchronization?
It supports constraints, retries and reliable deferred execution, including work that should continue after process death.
10. How do you prevent duplicate API operations during retry?
Assign each operation a unique idempotency key and let the server return the result of the original operation instead of performing it again.
11. Should every API response be displayed directly?
In an offline-first architecture, it should usually be stored in the local database first. The UI then receives the update from the database.
12. Can all banking operations work offline?
No. Sensitive and transactional operations such as payments, OTP verification and final balance confirmation generally require validation by the server.
Quick Revision
Cache strategy
→ Decides whether data comes from cache, network or both
Stale-data indication
→ Tells the user that displayed data may be outdated
Synchronization
→ Keeps local and remote data consistent
Conflict resolution
→ Decides what to do when local and remote data both change
Source of truth
→ The authoritative source observed by the application UI
Recommended offline-first flow
→ API → Room → ViewModel → UI
Offline write
→ UI → Room → Pending queue → WorkManager → API