WorkManager is Android's recommended API for deferrable background work that must run even if the app is killed or the device restarts. It's built on JobScheduler, AlarmManager, and BroadcastReceiver internally, selecting the right mechanism per API level automatically.
Core Concepts
- OneTimeWorkRequest — runs once; can be retried
- PeriodicWorkRequest — repeats on an interval (minimum 15 minutes)
- Constraints — preconditions (network, charging, battery, storage)
- Chaining — sequence of workers where output feeds input
- Unique Work — deduplicate by name to prevent concurrent duplicate jobs
Basic CoroutineWorker
class UploadArticleWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val articleId = inputData.getString("article_id") ?: return Result.failure()
return try {
val article = dao.getById(articleId) ?: return Result.failure()
api.uploadArticle(article)
Result.success()
} catch (e: IOException) {
if (runAttemptCount < 3) Result.retry() else Result.failure()
}
}
}
Constraints
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // need any network
.setRequiresBatteryNotLow(true) // don't run on low battery
.setRequiresCharging(false) // can run without charger
.setRequiresStorageNotLow(true) // don't run when storage is critical
.build()
val uploadRequest = OneTimeWorkRequestBuilder<UploadArticleWorker>()
.setInputData(workDataOf("article_id" to article.id))
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.addTag("upload")
.build()
WorkManager.getInstance(context).enqueue(uploadRequest)
Chaining Workers
Chain sequentially for pipelines where each step depends on the previous:
// Step 1: Compress image → Step 2: Upload → Step 3: Notify server
val compressWork = OneTimeWorkRequestBuilder<CompressImageWorker>()
.setInputData(workDataOf("image_path" to path))
.build()
val uploadWork = OneTimeWorkRequestBuilder<UploadImageWorker>().build()
val notifyWork = OneTimeWorkRequestBuilder<NotifyServerWorker>().build()
WorkManager.getInstance(context)
.beginWith(compressWork)
.then(uploadWork)
.then(notifyWork)
.enqueue()
Pass data between workers via outputData:
class CompressImageWorker(...) : CoroutineWorker(...) {
override suspend fun doWork(): Result {
val inputPath = inputData.getString("image_path") ?: return Result.failure()
val outputPath = compress(inputPath)
return Result.success(workDataOf("compressed_path" to outputPath))
}
}
class UploadImageWorker(...) : CoroutineWorker(...) {
override suspend fun doWork(): Result {
val path = inputData.getString("compressed_path") ?: return Result.failure()
// ... upload the compressed file
return Result.success()
}
}
Parallel Fan-out then Merge
val downloadA = OneTimeWorkRequestBuilder<DownloadAWorker>().build()
val downloadB = OneTimeWorkRequestBuilder<DownloadBWorker>().build()
val mergeWork = OneTimeWorkRequestBuilder<MergeWorker>().build()
WorkManager.getInstance(context)
.beginWith(listOf(downloadA, downloadB)) // run in parallel
.then(mergeWork) // wait for both, then merge
.enqueue()
Unique Work
Prevent duplicate syncs:
WorkManager.getInstance(context).enqueueUniqueWork(
"article_sync", // unique name
ExistingWorkPolicy.KEEP, // KEEP running, REPLACE with new, or APPEND
OneTimeWorkRequestBuilder<SyncWorker>().build()
)
// For periodic:
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"daily_sync",
ExistingPeriodicWorkPolicy.KEEP,
PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.DAYS).build()
)
Observing Work State
WorkManager.getInstance(context)
.getWorkInfoByIdLiveData(uploadRequest.id)
.observe(viewLifecycleOwner) { info ->
when (info?.state) {
WorkInfo.State.ENQUEUED -> showQueued()
WorkInfo.State.RUNNING -> showProgress()
WorkInfo.State.SUCCEEDED -> showSuccess()
WorkInfo.State.FAILED -> showError()
WorkInfo.State.CANCELLED -> showCancelled()
else -> Unit
}
}
Key Takeaways
| Concept | Rule |
|---|---|
CoroutineWorker | Prefer over Worker for Kotlin projects |
Result.retry() | Check runAttemptCount < N before retrying forever |
| Constraints | Always set NetworkType.CONNECTED for network-dependent work |
| Unique work | Use enqueueUniqueWork to avoid duplicate background jobs |
| Chaining | beginWith().then().then().enqueue() for sequential pipelines |
| Periodic minimum | 15 minutes is the WorkManager minimum interval |