skip to Main Content

Stop Shipping Stale Data: Backend-Driven Cache Updates on Android

March 15, 20264 minute read

  

Caching in Android is not just about storing data — it’s about keeping it fresh. Local storage like SharedPreferences, Room, or files does not provide automatic mechanisms to refresh cached data.

The solution: WorkManager, combined with a backend-controlled maxAge, which defines how long the cached data should remain fresh before being updated.

Not a member? Read the full article for free here →

Why Cache Updates Matter

Imagine caching API responses locally. Without periodic updates:

  • Users might see outdated information
  • Features depending on cached data could behave incorrectly
  • UI state could become inconsistent with the backend

Automatic cache updates ensure your app stays in sync with backend data without forcing users to manually reload.

Using WorkManager for Cache Updates

WorkManager is ideal for background tasks that must run reliably, even if the app is killed or the device restarts.

For cache updates, we can use OneTimeWorkRequest:

  • It allows precise scheduling of updates after a maxAge duration.
  • Ensures that updates run at the right time.
  • Avoids minimum interval restrictions that come with periodic work.

Step 1: Define a Generic Cache Updater

abstract class CacheUpdater<T> {
// Trigger a cache update for a key
protected abstract fun updateCache(key: String, maxAge: Duration)
// Save data and schedule an update based on maxAge
abstract fun saveAndUpdateCache(key: String, data: T, maxAge: Duration)
}

✅ Key point: The update schedule is driven by maxAge, which can come directly from backend HTTP headers (Cache-Control: max-age=600).

Step 2: Implement for SharedPreferences

class SharedPreferencesCacheUpdater(
@ApplicationContext private val context: Context,
private val sharedPreferences: SharedPreferences
) : CacheUpdater<String>() {
override fun updateCache(key: String, maxAge: Duration) {
CacheUpdateWorker.scheduleWorker(context, key, maxAge)
}
override fun saveAndUpdateCache(key: String, data: String, maxAge: Duration) {
sharedPreferences.edit().putString(key, data).apply()
updateCache(key, maxAge)
}
}
  • Saves data locally.
  • Schedules a background update after maxAge duration.

Step 3: Create the Cache Update Worker

@HiltWorker
class CacheUpdateWorker @AssistedInject constructor(
@Assisted private val context: Context,
@Assisted workerParams: WorkerParameters,
private val sharedPreferences: SharedPreferences
) : CoroutineWorker(context, workerParams) {
override suspend fun doWork(): Result {
val key = inputData.getString(INPUT_KEY)
if (key != null) {
try {
// Fetch updated data from backend or repository
val updatedData = fetchFromBackend(key)
sharedPreferences.edit().putString(key, updatedData).apply()
} catch (e: Exception) {
// API failed: retain stale data by default
// Optionally, you could clear the data with:
// sharedPreferences.edit().putString(key, null).apply()
}
}
return Result.success()
}
companion object {
private const val INPUT_KEY = "KEY"
private const val WORKER_TAG = "Cache_Update_Worker"
fun scheduleWorker(context: Context, key: String, maxAge: Duration) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
// Each worker is tagged with the key it updates
val tag = "$WORKER_TAG_$key"
val workerRequest = OneTimeWorkRequestBuilder<CacheUpdateWorker>()
.setInputData(Data.Builder().putString(INPUT_KEY, key).build())
.setConstraints(constraints)
.setInitialDelay(maxAge) // Schedule update after maxAge duration
.addTag(tag)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
tag,
ExistingWorkPolicy.REPLACE, // Replace previous worker for the same key
workerRequest
)
}
private suspend fun fetchFromBackend(key: String): String {
// Replace with repository/network call
return "Updated data for $key"
}
}
}

Behavior on API Failure

  • Default: If the API call fails, the existing cached (stale) data is kept. This ensures your app always has something to show.
  • Optional: You can reset the cached data to null by uncommenting:
sharedPreferences.edit().putString(key, null).apply()

This gives you full control over how stale data is handled.

Step 4: Using the Cache Updater

val sharedPreferences = context.getSharedPreferences("app_prefs", Context.MODE_PRIVATE)
val cacheUpdater = SharedPreferencesCacheUpdater(context, sharedPreferences)
// Save data and schedule update based on backend maxAge
val maxAge = Duration.ofMinutes(10) // Could come from Cache-Control header
cacheUpdater.saveAndUpdateCache("user_profile", "initial data", maxAge)
// Trigger a manual update if needed
cacheUpdater.updateCache("user_profile", maxAge)
  • Saves data locally.
  • Automatically fetches updated data after the backend-specified maxAge.
  • Uses stale data in case of network/API failure, unless explicitly cleared.

Backend-Controlled Cache Updates

Using HTTP headers like:

Cache-Control: max-age=600, stale-while-revalidate=60

you can:

  • Schedule updates dynamically based on server-defined freshness.
  • Serve stale data while fetching updates in the background.
  • Adjust cache lifetimes without releasing a new app version.

This makes caching fully backend-driven and ensures your app data stays consistent.

Why This Approach Works

  • Reliable: WorkManager ensures updates run even if the app is killed.
  • Key-based isolation: Workers are tagged per key to prevent conflicts.
  • Resettable schedule: ExistingWorkPolicy.REPLACE avoids duplicate updates.
  • Backend-driven: Use maxAge headers to dynamically control cache freshness.
  • Handles API failures gracefully: Keeps stale data if the backend is unavailable, or optionally clears the data.
  • Flexible: Works for SharedPreferences, Room, files, or other local storage.

This pattern turns caching into a dynamic, backend-controlled system, keeping your Android app data fresh and consistent without manual intervention.


Stop Shipping Stale Data: Backend-Driven Cache Updates on Android was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.

 

Web Developer, Web Design, Web Builder, Project Manager, Business Analyst, .Net Developer

No Comments

This Post Has 0 Comments

Leave a Reply

Back To Top