Skip to content
intermediate Phase 5 · Jetpack Libraries

WorkManager

Schedule deferrable, guaranteed background work with WorkManager and constraints.

50m
3 problems
Topic Progress 0%

Workers and WorkRequests

What WorkManager Does

WorkManager is for work that must run eventually — even if the app exits or the device restarts. It is not for immediate work; use coroutines for that. WorkManager handles:

  • Guaranteed execution: work persists and runs when constraints are met
  • Backward compatibility: uses JobScheduler on API 23+, AlarmManager + BroadcastReceiver on older APIs
  • Observable work: track status, progress, and results

Defining a Worker

class SyncWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        val userId = inputData.getString("userId") ?: return Result.failure()

        return try {
            repository.syncUserData(userId)
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

CoroutineWorker gives you a suspend context. doWork() returns:

  • Result.success() — work completed
  • Result.failure() — permanent failure
  • Result.retry() — reschedule with backoff

Enqueueing Work

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

WorkManager.getInstance(context).enqueue(syncRequest)

Periodic Work

For recurring tasks like daily sync:

val periodicSync = PeriodicWorkRequestBuilder<DailySyncWorker>(
    1, TimeUnit.HOURS,  // repeat interval
    15, TimeUnit.MINUTES // flex interval
).build()

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "daily_sync",
        ExistingPeriodicWorkPolicy.KEEP,
        periodicSync
    )

The flex interval defines when the work can run within the repeat interval. Work runs once during the last 15 minutes of each hour.

Chaining, Tags, and Observing

Work Chaining

Chain work to run sequentially. Each worker receives the output of the previous one as input:

WorkManager.getInstance(context)
    .beginWith(downloadWork)
    .then(processWork)
    .then(uploadWork)
    .enqueue()

For parallel branches that converge:

val parallelWork = WorkManager.getInstance(context)
    .beginWith(downloadImages)
    .then(downloadVideos)
    .enqueue()

WorkManager.getInstance(context)
    .beginWith(parallelWork)
    .then(mergeWork)
    .enqueue()

Tags

Tag work for bulk operations:

val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
    .addTag("upload")
    .addTag("media")
    .build()

// Cancel all work with a tag
WorkManager.getInstance(context).cancelAllWorkByTag("upload")

// Observe all work with a tag
WorkManager.getInstance(context)
    .getWorkInfosByTagLiveData("upload")
    .observe(lifecycleOwner) { workList ->
        val running = workList.any { it.state == WorkInfo.State.RUNNING }
    }

Observing Work Status

WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(syncRequest.id)
    .observe(lifecycleOwner) { workInfo ->
        when (workInfo?.state) {
            WorkInfo.State.ENQUEUED -> { /* waiting */ }
            WorkInfo.State.RUNNING -> { /* executing */ }
            WorkInfo.State.SUCCEEDED -> {
                val result = workInfo.outputData.getString("result")
            }
            WorkInfo.State.FAILED -> { /* permanent failure */ }
            WorkInfo.State.CANCELLED -> { /* cancelled */ }
            else -> {}
        }
    }

Unique Work

Prevent duplicate work by name:

// Only one sync can run at a time
WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "user_sync",
        ExistingWorkPolicy.REPLACE,  // cancel previous, run new
        syncRequest
    )

// Options: KEEP (skip new), REPLACE (cancel old), APPEND (queue after)

Input and Output Data

Workers communicate through Data objects (key-value pairs limited to primitives):

class ProcessWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        val inputUrl = inputData.getString("url") ?: return Result.failure()
        val processed = processUrl(inputUrl)
        val outputData = workDataOf("filePath" to processed)
        return Result.success(outputData)
    }
}

Chained workers automatically pass output as input to the next worker.

Quiz

1. When should you use WorkManager instead of coroutines?

Question 1 options

2. What does Result.retry() do in a CoroutineWorker?

Question 2 options

3. What is the difference between ExistingWorkPolicy.REPLACE and ExistingWorkPolicy.KEEP?

Question 3 options

4. What is the maximum repeat interval for PeriodicWorkRequest?

Question 4 options

Flashcards

Question

What is the difference between OneTimeWorkRequest and PeriodicWorkRequest?

Answer

OneTimeWorkRequest runs once. PeriodicWorkRequest repeats at a fixed interval with a minimum of 15 minutes. Both support constraints and chaining.

Question

When should you use enqueueUniqueWork vs enqueueUniquePeriodicWork?

Answer

enqueueUniqueWork for one-time unique work (with REPLACE/KEEP/APPEND policies). enqueueUniquePeriodicWork for repeating unique work with the same policies.

Question

How do Workers communicate data to each other in a chain?

Answer

Through input and output Data objects. The output of one worker becomes the input of the next worker in the chain.

Question

What constraints can you set on a WorkRequest?

Answer

Network type, battery level, storage level, charging state, idle state, and device charging. Set via Constraints.Builder().

Revision Notes

Key Takeaways

  • 1. WorkManager guarantees eventual execution even after app exit or device restart
  • 2. Use CoroutineWorker for suspend-based background work with Result.success/failure/retry
  • 3. Chain work sequentially with beginWith().then() for multi-step workflows
  • 4. Use enqueueUniqueWork to prevent duplicate executions by name
  • 5. PeriodicWorkRequest has a minimum repeat interval of 15 minutes

Interview Tips

  • Explain when WorkManager is appropriate vs using coroutines directly
  • Discuss work chaining and how data flows between workers
  • Know the difference between REPLACE, KEEP, and APPEND policies
  • Be ready to design a background sync system with constraints and retry

Cheat Sheet

WorkManager Cheat Sheet

Worker Types:

  • CoroutineWorker — suspend-based, preferred
  • RxWorker — RxJava-based
  • Worker — blocking, legacy

WorkRequest:

  • OneTimeWorkRequestBuilder<T>() — run once
  • PeriodicWorkRequestBuilder<T>(interval, unit) — repeat (min 15 min)
  • .setInputData(workDataOf(...)) — pass data to worker
  • .setConstraints(...) — set execution conditions
  • .addTag("tag") — tag for bulk operations

Enqueueing:

  • .enqueue(request) — fire and forget
  • .enqueueUniqueWork(name, policy, request) — prevent duplicates
  • .enqueueUniquePeriodicWork(name, policy, request) — periodic unique

Policies:

  • ExistingWorkPolicy.KEEP — skip if exists
  • ExistingWorkPolicy.REPLACE — cancel existing, run new
  • ExistingWorkPolicy.APPEND — queue after existing

Chaining:

  • .beginWith(a).then(b).then(c) — sequential
  • Parallel branches: multiple .beginWith() calls
  • Output of one worker = input of next

Result:

  • Result.success(data) — done
  • Result.failure(data) — permanent failure
  • Result.retry() — reschedule with backoff