Skip to content
advanced Phase 9 · Background Processing

WorkManager Advanced

Chain work, use constraints, handle unique work, and observe work status with WorkManager.

45m
2 problems
Topic Progress 0%

Chaining Work and Applying Constraints

Work Chaining

WorkManager lets you link work requests so that each step runs only after the previous one succeeds. This creates a dependency graph without manually managing execution order.

val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>().build()
val compressWork = OneTimeWorkRequestBuilder<CompressWorker>().build()
val notifyWork = OneTimeWorkRequestBuilder<NotifyWorker>().build()

WorkManager.getInstance(context)
    .beginWith(uploadWork)
    .then(compressWork)
    .then(notifyWork)
    .enqueue()

// Execution order: upload → compress → notify
// If upload fails, compress and notify are skipped

Parallel Chaining

When multiple work items have no dependency on each other, run them in parallel:

val syncPhotos = OneTimeWorkRequestBuilder<SyncPhotosWorker>().build()
val syncContacts = OneTimeWorkRequestBuilder<SyncContactsWorker>().build()
val syncCalendar = OneTimeWorkRequestBuilder<SyncCalendarWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(syncPhotos, syncContacts, syncCalendar))  // parallel
    .then(OneTimeWorkRequestBuilder<FinalSyncWorker>().build()) // sequential after all
    .enqueue()

Constraints

Constraints define conditions that must be met before work executes. WorkManager automatically defers work until all constraints are satisfied.

val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.UNMETERED)   // Wi-Fi only
    .setRequiresBatteryNotLow(true)                   // skip if battery low
    .setRequiresCharging(true)                        // only while charging
    .setRequiresStorageNotLow(true)                   // skip if storage low
    .setRequiresDeviceIdle(true)                      // Android 6.0+ Doze mode
    .build()

val workRequest = OneTimeWorkRequestBuilder<BackupWorker>
    .setConstraints(constraints)
    .setBackoffCriteria(
        BackoffPolicy.EXPONENTIAL,
        WorkRequest.MIN_BACKOFF_MILLIS,
        TimeUnit.MILLISECONDS
    )
    .build()

Constraint Behavior

Constraint Min API Effect
UNMETERED 14 Wi-Fi only
NOT_ROAMING 28 Home network only
CHARGING Battery charging
NOT_LOW 24 Battery above low threshold
STORAGE_NOT_LOW 24 Sufficient storage
DEVICE_IDLE 23 Device not in Doze

Initial Delay

Delay work without creating a new constraint:

val delayedWork = OneTimeWorkRequestBuilder<DailyReportWorker>
    .setInitialDelay(1, TimeUnit.HOURS)
    .build()

Unique Work and Observing Status

Unique Work

Unique work prevents duplicate operations. If a request with the same unique name exists, WorkManager applies the specified policy.

// REPLACE: Cancel existing, enqueue new
WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "DatabaseBackup",
        ExistingWorkPolicy.REPLACE,
        OneTimeWorkRequestBuilder<BackupWorker>().build()
    )

// KEEP: Ignore new request if existing is active
WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "DatabaseBackup",
        ExistingWorkPolicy.KEEP,
        OneTimeWorkRequestBuilder<BackupWorker>().build()
    )

// APPEND: Add new work after existing finishes
WorkManager.getInstance(context)
    .enqueueUniqueWork(
        "DatabaseBackup",
        ExistingWorkPolicy.APPEND,
        OneTimeWorkRequestBuilder<BackupWorker>().build()
    )

Periodic work uses enqueueUniquePeriodicWork — same concept, different method:

WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork(
        "SyncData",
        ExistingPeriodicWorkPolicy.KEEP,
        PeriodicWorkRequestBuilder<SyncWorker>(
            15, TimeUnit.MINUTES  // minimum interval
        ).setConstraints(
            Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()
        ).build()
    )

Observing Work Status

Observe work status using WorkInfo to react to completion, failure, or progress:

// Observe a specific work request
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(uploadRequest.id)
    .observe(this) { workInfo ->
        when (workInfo?.state) {
            WorkInfo.State.ENQUEUED -> showQueued()
            WorkInfo.State.RUNNING -> showProgress(workInfo.progress)
            WorkInfo.State.SUCCEEDED -> showSuccess()
            WorkInfo.State.FAILED -> showError(workInfo.outputData)
            WorkInfo.State.CANCELLED -> showCancelled()
            else -> {}
        }
    }

// Observe unique work by name
WorkManager.getInstance(context)
    .getWorkInfosForUniqueWorkLiveData("DatabaseBackup")
    .observe(this) { workInfos ->
        val allSucceeded = workInfos.all { it.state == WorkInfo.State.SUCCEEDED }
        if (allSucceeded) updateUI()
    }

Progress Updates

Workers can report progress during execution:

class UploadWorker : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        val files = getInputData().getStringArray("files") ?: return Result.failure()

        files.forEachIndexed { index, file ->
            uploadFile(file)
            setProgress(
                Data.Builder()
                    .putInt("progress", (index + 1) * 100 / files.size)
                    .putString("currentFile", file)
                    .build()
            )
        }

        return Result.success(
            Data.Builder()
                .putInt("uploadedCount", files.size)
                .build()
        )
    }
}

Input and Output Data

Workers communicate through Data bundles:

// Enqueue with input
val request = OneTimeWorkRequestBuilder<ProcessWorker>
    .setInputData(
        Data.Builder()
            .putString("inputPath", "/sdcard/photo.jpg")
            .putInt("quality", 85)
            .build()
    )
    .build()

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

Cancellation

// Cancel specific work
WorkManager.getInstance(context).cancelWorkById(uploadRequest.id)

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

// Cancel unique work
WorkManager.getInstance(context).cancelUniqueWork("DatabaseBackup")

Quiz

1. What does `ExistingWorkPolicy.REPLACE` do in `enqueueUniqueWork`?

Question 1 options

2. What is the minimum interval for PeriodicWorkRequest?

Question 2 options

3. In a work chain `beginWith(A).then(B).then(C)`, what happens if B fails?

Question 3 options

4. How can a Worker report progress during execution?

Question 4 options

Flashcards

Question

What is the difference between enqueueUniqueWork and enqueueUniquePeriodicWork?

Answer

enqueueUniqueWork is for one-time requests. enqueueUniquePeriodicWork is for periodic work that repeats. Both use unique names to prevent duplicates, but periodic work repeats at a minimum 15-minute interval.

Question

When should you use ExistingWorkPolicy.KEEP vs REPLACE?

Answer

KEEP when the existing work is the authoritative version and you want to ignore duplicates (e.g., ongoing sync). REPLACE when the new request supersedes the old (e.g., latest backup).

Question

How do constraints affect queued work?

Answer

WorkManager automatically defers work until all constraints are satisfied. When constraints change (e.g., device connects to Wi-Fi), queued work starts without being re-enqueued.

Question

What does a CoroutineWorker return on success?

Answer

Result.success(Data) with optional output data. Return Result.failure() for permanent errors or Result.retry() to schedule a retry with backoff.

Revision Notes

Key Takeaways

  • 1. WorkManager chains let you create dependency graphs — parallel steps followed by sequential final steps
  • 2. ExistingWorkPolicy.KEEP prevents duplicate work; REPLACE cancels old work for new requests
  • 3. Constraints automatically defer work until conditions are met without re-enqueuing
  • 4. Observe work status with LiveData to react to completion, failure, and progress

Interview Tips

  • Explain when to use WorkManager vs foreground services vs coroutines — a common design question
  • Know the difference between unique work policies (KEEP, REPLACE, APPEND)
  • Be ready to describe a real use case: chained work for upload → compress → notify
  • Mention the 15-minute minimum for periodic work as a practical constraint

Cheat Sheet

WorkManager Advanced Cheat Sheet

Chaining:

  • beginWith(work).then(work).then(work) — sequential
  • beginWith(listOf(work1, work2)) — parallel, then sequential
  • Chain fails if any step fails

Unique Work Policies:

  • REPLACE: Cancel old, enqueue new
  • KEEP: Ignore new if old exists
  • APPEND: Run new after old finishes

Constraints:

  • UNMETERED, NOT_ROAMING, CHARGING, NOT_LOW, STORAGE_NOT_LOW, DEVICE_IDLE
  • WorkManager auto-starts work when constraints are met

Observing Status:

  • getWorkInfoByIdLiveData — observe specific work
  • getWorkInfosForUniqueWorkLiveData — observe by unique name
  • States: ENQUEUED, RUNNING, SUCCEEDED, FAILED, CANCELLED

Progress:

  • Worker calls setProgress(Data) during execution
  • Observer reads workInfo.progress

Input/Output:

  • setInputData(Data) when enqueuing
  • Read outputData after SUCCEEDED

Cancellation:

  • cancelWorkById — specific request
  • cancelAllWorkByTag — all work with tag
  • cancelUniqueWork — by unique name