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

StrategyFirst sourceFallbackBest use
Cache firstLocal cacheNetworkData that changes slowly
Network firstServerLocal cacheFresh data is important
Cache and networkCache, then serverCached dataFast and responsive screens
Network onlyServerUsually errorReal-time sensitive operations
Cache onlyLocal storageUsually errorDownloaded 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 data

The 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 AM

Data 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:

DataPossible cache validity
Static configurationSeveral days
Product catalogueSeveral hours
News feedA few minutes
Account balanceVery short duration
Payment confirmationMust 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 database

This downloads new or updated server data.

Local database → Server

This 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 change

Suitable 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 change

Suitable 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 wins

Advantage: 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 → conflict

Request 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 address

This 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

StrategyResolutionSuitable for
Server winsServer data replaces local dataBalance, payment and permissions
Client winsLocal data replaces server dataPersonal drafts and preferences
Last write winsLatest update is selectedSimple, low-risk records
Version basedVersion mismatch detects conflictBusiness and enterprise applications
Field-level mergeIndependent fields are combinedProfiles and editable forms
Manual resolutionUser selects the correct versionImportant 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 --> D

Read flow

Room → Repository → ViewModel → UI

Refresh flow

API → Repository → Room → ViewModel → UI

Offline-write flow

UI → ViewModel → Repository → Room
                               ↓
                         Pending sync
                               ↓
                        API when online


7. 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 --> API

Responsibilities

ComponentResponsibility
UIDisplays data, offline status and sync status
ViewModelManages UI state and user actions
RepositoryCoordinates local and remote data
RoomProvides persistent data to the UI
APIProvides remote authoritative data
WorkManagerExecutes reliable deferred synchronization
Conflict resolverDecides 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 → UI

This creates multiple competing data sources.

Use:

API → Room → UI


2. 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