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`?
2. What is the minimum interval for PeriodicWorkRequest?
3. In a work chain `beginWith(A).then(B).then(C)`, what happens if B fails?
4. How can a Worker report progress during execution?
Flashcards
Question
What is the difference between enqueueUniqueWork and enqueueUniquePeriodicWork?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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