androidengineers.Book a session

Threading & Concurrency

WorkManager for Deferrable Work

article20 minMedium

WorkManager is the Jetpack library for guaranteed background execution — work that must eventually complete even if the app exits, the device restarts, or the process is killed. It is the right tool for a specific class of tasks. Understanding when to use it — and when not to — is as important as knowing its API.

What WorkManager Is (and Isn't)

WorkManager guarantees execution by persisting work to a database. If the process dies, work is rescheduled when the app restarts. It uses JobScheduler on API 23+ internally, falling back to AlarmManager on older devices.

Use WorkManager for:

  • Uploading analytics/logs when the device gets a network connection
  • Syncing offline changes to the server
  • Compressing and uploading photos in the background
  • Sending a notification at a scheduled time even if the app is closed

Don't use WorkManager for:

  • Work that must run at an exact millisecond (use AlarmManager.setExact)
  • Work that must run right now while the user is watching (use coroutines)
  • UI-reactive work triggered by user interaction (use coroutines)
  • Periodic polling at sub-15-minute intervals (OS won't allow it)

Your First Worker

class SyncWorker(
    appContext: Context,
    params: WorkerParameters
) : CoroutineWorker(appContext, params) {  // CoroutineWorker runs on Dispatchers.IO by default

    override suspend fun doWork(): Result {
        return try {
            val pendingItems = localDb.pendingChanges()
            remoteApi.sync(pendingItems)
            localDb.markSynced(pendingItems)
            Result.success()
        } catch (e: IOException) {
            if (runAttemptCount < 3) {
                Result.retry()   // WorkManager will retry with exponential backoff
            } else {
                Result.failure(
                    workDataOf("error" to e.message)
                )
            }
        }
    }
}

Return values:

  • Result.success() — work is done; won't run again (for OneTimeWork)
  • Result.retry() — failed; try again later with backoff
  • Result.failure() — permanently failed; don't retry

OneTimeWorkRequest vs PeriodicWorkRequest

OneTimeWorkRequest

Runs once. Can be chained, cancelled, queried.

val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .setRequiresBatteryNotLow(true)
            .build()
    )
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .setInputData(workDataOf("userId" to userId))
    .build()

WorkManager.getInstance(context).enqueue(syncRequest)

PeriodicWorkRequest

Repeats at a minimum interval of 15 minutes (OS-enforced).

val periodicSync = PeriodicWorkRequestBuilder<SyncWorker>(
    repeatInterval = 1,
    repeatIntervalTimeUnit = TimeUnit.HOURS
)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "hourly-sync",
        ExistingPeriodicWorkPolicy.KEEP,  // don't replace if already queued
        periodicSync
    )

ExistingPeriodicWorkPolicy.KEEP — preserve the existing schedule. ExistingPeriodicWorkPolicy.REPLACE — cancel and re-enqueue (resets the timer). ExistingPeriodicWorkPolicy.UPDATE — update constraints/data without resetting timer.

Constraints

Constraints let WorkManager wait for the right conditions before running:

Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)       // any network
    .setRequiredNetworkType(NetworkType.UNMETERED)       // WiFi only
    .setRequiresBatteryNotLow(true)                      // > ~15% battery
    .setRequiresCharging(true)                           // plugged in
    .setRequiresStorageNotLow(true)                      // disk not full
    .setRequiresDeviceIdle(true)                         // API 23+; device idle
    .build()

WorkManager monitors these conditions continuously. If constraints are met, work runs. If a constraint is lost mid-work (e.g., network drops), the work is interrupted and rescheduled.

Passing Data In and Out

// Input data
val request = OneTimeWorkRequestBuilder<CompressWorker>()
    .setInputData(workDataOf(
        "imagePath" to "/storage/image.jpg",
        "quality" to 80
    ))
    .build()

// In the Worker
class CompressWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
    override suspend fun doWork(): Result {
        val path = inputData.getString("imagePath") ?: return Result.failure()
        val quality = inputData.getInt("quality", 85)
        val outputPath = compress(path, quality)
        // Output data passed to chained workers or observers
        return Result.success(workDataOf("outputPath" to outputPath))
    }
}

// Observe output
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(request.id)
    .observe(this) { info ->
        if (info?.state == WorkInfo.State.SUCCEEDED) {
            val path = info.outputData.getString("outputPath")
            displayResult(path)
        }
    }

Data values are limited to 10 KB total — pass identifiers (IDs, paths), not large payloads.

Chaining Work

val download = OneTimeWorkRequestBuilder<DownloadWorker>().build()
val compress = OneTimeWorkRequestBuilder<CompressWorker>().build()
val upload   = OneTimeWorkRequestBuilder<UploadWorker>().build()

WorkManager.getInstance(context)
    .beginWith(download)
    .then(compress)      // runs after download succeeds; receives download's output
    .then(upload)        // runs after compress succeeds
    .enqueue()

Parallel then sequential:

val thumbnails = OneTimeWorkRequestBuilder<ThumbnailWorker>().build()
val metadata   = OneTimeWorkRequestBuilder<MetadataWorker>().build()
val save       = OneTimeWorkRequestBuilder<SaveWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(thumbnails, metadata))  // both run in parallel
    .then(save)                               // runs after BOTH complete
    .enqueue()

Unique Work: Preventing Duplicates

Use enqueueUniqueWork to ensure only one instance of a task runs at a time:

WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "photo-upload",
        ExistingWorkPolicy.APPEND_OR_REPLACE, // cancel existing, start fresh
        uploadRequest
    )

Policies:

  • KEEP — ignore new request if one is already pending/running
  • REPLACE — cancel existing, enqueue new
  • APPEND — chain new work after existing
  • APPEND_OR_REPLACE — append if running, replace if failed/cancelled

Reporting Progress

For long-running workers, report progress back to the UI:

class UploadWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
    override suspend fun doWork(): Result {
        val files = inputData.getStringArray("files") ?: return Result.failure()
        files.forEachIndexed { index, file ->
            upload(file)
            val progress = ((index + 1).toFloat() / files.size * 100).toInt()
            setProgress(workDataOf("progress" to progress))
        }
        return Result.success()
    }
}

// Observe in UI
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(request.id)
    .observe(this) { info ->
        val progress = info?.progress?.getInt("progress", 0) ?: 0
        progressBar.progress = progress
    }

Key Takeaways

ConceptRule
When to use WorkManagerGuaranteed execution, constraints, process-death-safe
When NOT to useExact timing, immediate UI work, < 15 min intervals
Result.retry()Triggers exponential backoff; respect runAttemptCount
PeriodicWork min interval15 minutes (OS-enforced)
Data limit10 KB per input/output payload
Unique workPrevents duplicate queuing of the same logical task
ChainingOutput of previous worker becomes input of next
ProgresssetProgress() in worker; observe via WorkInfo.progress

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
WorkManager for Deferrable Work | Android System Design | Android Engineers