WorkManager Tutorial — Student Friendly
1. What is WorkManager?
WorkManager is an Android Jetpack library used to run background work reliably.
Use it when work should complete even if:
App is closed App process is killed Device restarts Network is temporarily unavailable Work needs retry
Android describes WorkManager as the recommended solution for persistent background work that should run either immediately or later when constraints are met.
2. Simple real-life analogy
Think of WorkManager like a task scheduler/helper.
You give task to WorkManager: "Upload this report when internet is available." Then WorkManager says: "Okay, I will do it. If app closes, I remember it. If network is not available, I wait. If upload fails, I retry."
So:
Activity/ViewModel = gives work WorkManager = manages work Worker = performs actual task Constraints = conditions for work Result = success / failure / retry
3. When should we use WorkManager?
Use WorkManager for:
Upload logs Sync data Backup data Download small/medium files Send pending analytics Retry failed API calls Daily background sync Database cleanup Image compression before upload
Do not use WorkManager for:
Exact alarm at exact time Music playback Ongoing navigation Bluetooth live connection Real-time chat socket Foreground-only UI task
For user-visible ongoing work like music or navigation, use Foreground Service. For exact alarm, use AlarmManager. For simple UI-related short work, use coroutine in ViewModel.
4. WorkManager basic architecture
Activity / ViewModel | | creates WorkRequest v WorkManager | | schedules work v Worker | | does actual task v Result.success() Result.failure() Result.retry()
5. Main classes in WorkManager
| Class | Meaning |
|---|---|
Worker | Performs background task synchronously |
CoroutineWorker | Performs background task using Kotlin coroutine |
WorkRequest | Request object that describes work |
OneTimeWorkRequest | Work runs once |
PeriodicWorkRequest | Work runs repeatedly |
Constraints | Conditions like network, charging, battery |
WorkManager | Enqueues, observes, cancels work |
WorkInfo | Current state/progress of work |
Data | Input/output key-value data |
6. Worker states
WorkManager work moves through states:
ENQUEUED | v RUNNING | +----> SUCCEEDED | +----> FAILED | +----> RETRY / ENQUEUED again | +----> CANCELLED
Meaning:
| State | Meaning |
|---|---|
ENQUEUED | Waiting to run |
RUNNING | Currently executing |
SUCCEEDED | Completed successfully |
FAILED | Failed permanently |
CANCELLED | Cancelled by app/user |
BLOCKED | Waiting for previous work in chain |
7. Add WorkManager dependency
In build.gradle:
dependencies { implementation("androidx.work:work-runtime-ktx:2.10.0") }
Use the latest stable version compatible with your project.
8. First simple Worker
Example: upload logs.
class LogUploadWorker( context: Context, workerParams: WorkerParameters ) : CoroutineWorker(context, workerParams) { override suspend fun doWork(): Result { return try { uploadLogs() Result.success() } catch (e: Exception) { Result.retry() } } private suspend fun uploadLogs() { delay(3000) Log.d("LogUploadWorker", "Logs uploaded") } }
Important method:
override suspend fun doWork(): Result
This is where actual background task runs.
9. Enqueue one-time work
val uploadRequest = OneTimeWorkRequestBuilder<LogUploadWorker>() .build() WorkManager.getInstance(context) .enqueue(uploadRequest)
Flow:
Create request | v Enqueue request | v WorkManager runs Worker | v Worker returns Result
10. Result types
Inside doWork() you return one of these:
Result.success() Result.failure() Result.retry()
Result.success()
Use when work completed.
return Result.success()
Example:
Logs uploaded successfully
Result.failure()
Use when work should not retry.
return Result.failure()
Example:
File not found Invalid user token Bad input data
Result.retry()
Use when failure is temporary.
return Result.retry()
Example:
No internet Server timeout HTTP 503 Temporary network error
11. Input data to Worker
Suppose you want to upload one file.
Create request with input data
val inputData = workDataOf( "file_uri" to fileUri.toString(), "user_id" to "101" ) val uploadRequest = OneTimeWorkRequestBuilder<FileUploadWorker>() .setInputData(inputData) .build() WorkManager.getInstance(context) .enqueue(uploadRequest)
Read input inside Worker
class FileUploadWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val fileUri = inputData.getString("file_uri") ?: return Result.failure() val userId = inputData.getString("user_id") ?: return Result.failure() uploadFile(fileUri, userId) return Result.success() } private suspend fun uploadFile(fileUri: String, userId: String) { delay(2000) Log.d("Upload", "Uploaded $fileUri for user $userId") } }
12. Output data from Worker
Worker can return output data after success.
override suspend fun doWork(): Result { val serverFileId = "FILE_123" val output = workDataOf( "server_file_id" to serverFileId ) return Result.success(output) }
Observe output:
WorkManager.getInstance(context) .getWorkInfoByIdLiveData(uploadRequest.id) .observe(this) { workInfo -> if (workInfo.state == WorkInfo.State.SUCCEEDED) { val fileId = workInfo.outputData.getString("server_file_id") Log.d("Output", "Uploaded file id = $fileId") } }
13. Constraints
Constraints mean:
Run this work only when conditions are satisfied.
Example:
Only when internet available Only when device charging Only when battery not low Only when storage not low
Network constraint
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val uploadRequest = OneTimeWorkRequestBuilder<FileUploadWorker>() .setConstraints(constraints) .build()
Meaning:
Run upload only when network is connected.
Wi-Fi/unmetered constraint
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .build()
Meaning:
Run only on Wi-Fi or unmetered network.
Useful for:
Large backup Video upload Database sync
Charging constraint
val constraints = Constraints.Builder() .setRequiresCharging(true) .build()
Meaning:
Run only when phone is charging.
Multiple constraints
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresCharging(true) .setRequiresBatteryNotLow(true) .build() val backupRequest = OneTimeWorkRequestBuilder<BackupWorker>() .setConstraints(constraints) .build()
Meaning:
Run backup only when: - Wi-Fi/unmetered network available - Device charging - Battery not low
14. Retry and backoff policy
If Worker returns:
Result.retry()
WorkManager retries later.
You can define retry delay strategy.
val uploadRequest = OneTimeWorkRequestBuilder<FileUploadWorker>() .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS ) .build()
Linear backoff
Retry after 30s, 60s, 90s...
Exponential backoff
Retry after 30s, 60s, 120s, 240s...
Use exponential for server/network failure.
15. Periodic WorkManager
Use PeriodicWorkRequest when task should repeat.
Example:
Sync data every 12 hours Upload logs daily Clean cache weekly
val syncRequest = PeriodicWorkRequestBuilder<SyncWorker>( 12, TimeUnit.HOURS ) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() ) .build() WorkManager.getInstance(context) .enqueue(syncRequest)
Important: Periodic work is not exact timing.
You say: run every 12 hours. Android decides best time around that window.
Do not use PeriodicWorkRequest for exact alarm like:
Run exactly at 8:00 AM sharp
For exact timing, use AlarmManager.
16. Unique work
Unique work prevents duplicate work.
Example problem:
User taps upload 5 times App creates 5 upload workers
Solution:
WorkManager.getInstance(context) .enqueueUniqueWork( "profile_photo_upload", ExistingWorkPolicy.REPLACE, uploadRequest )
ExistingWorkPolicy
| Policy | Meaning |
|---|---|
KEEP | Keep old work, ignore new one |
REPLACE | Cancel old work, start new one |
APPEND | Add new work after old work |
APPEND_OR_REPLACE | Append if possible, replace if chain failed/cancelled |
Example:
.enqueueUniqueWork( "daily_sync", ExistingWorkPolicy.KEEP, syncRequest )
Meaning:
If daily_sync is already scheduled, do not create duplicate.
17. Observe WorkManager status
WorkManager.getInstance(context) .getWorkInfoByIdLiveData(uploadRequest.id) .observe(this) { workInfo -> when (workInfo.state) { WorkInfo.State.ENQUEUED -> { textView.text = "Waiting..." } WorkInfo.State.RUNNING -> { textView.text = "Uploading..." } WorkInfo.State.SUCCEEDED -> { textView.text = "Upload complete" } WorkInfo.State.FAILED -> { textView.text = "Upload failed" } WorkInfo.State.CANCELLED -> { textView.text = "Upload cancelled" } else -> Unit } }
18. Observe progress
Inside Worker:
setProgress( workDataOf("progress" to 40) )
In Activity:
WorkManager.getInstance(context) .getWorkInfoByIdLiveData(uploadRequest.id) .observe(this) { workInfo -> val progress = workInfo.progress.getInt("progress", 0) progressBar.progress = progress textView.text = "$progress%" }
WorkManager supports intermediate progress through setProgress() in Kotlin CoroutineWorker; progress is stored asynchronously in WorkManager’s database.
19. Upload progress Worker example
class UploadWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { val fileUri = inputData.getString("file_uri") ?: return Result.failure() return try { uploadFile(fileUri) Result.success() } catch (e: IOException) { Result.retry() } catch (e: Exception) { Result.failure() } } private suspend fun uploadFile(fileUri: String) { val totalBytes = 100L var uploadedBytes = 0L while (uploadedBytes < totalBytes) { delay(500) uploadedBytes += 10 val progress = ((uploadedBytes * 100) / totalBytes).toInt() setProgress( workDataOf("progress" to progress) ) } } }
20. Long-running Worker with notification
For long upload/download, WorkManager can run as a foreground worker using setForeground(). Android documentation says WorkManager can support long-running workers by managing a foreground service on your behalf.
Worker with foreground notification
class LongUploadWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { companion object { private const val CHANNEL_ID = "upload_channel" private const val NOTIFICATION_ID = 501 } override suspend fun doWork(): Result { createNotificationChannel() setForeground(createForegroundInfo(0)) return try { for (progress in 1..100) { delay(300) setProgress(workDataOf("progress" to progress)) setForeground(createForegroundInfo(progress)) } Result.success() } catch (e: Exception) { Result.retry() } } private fun createForegroundInfo(progress: Int): ForegroundInfo { val notification = NotificationCompat.Builder( applicationContext, CHANNEL_ID ) .setSmallIcon(R.drawable.ic_upload) .setContentTitle("Uploading file") .setContentText("$progress% uploaded") .setProgress(100, progress, false) .setOnlyAlertOnce(true) .setOngoing(true) .build() return ForegroundInfo(NOTIFICATION_ID, notification) } private fun createNotificationChannel() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val channel = NotificationChannel( CHANNEL_ID, "Upload Progress", NotificationManager.IMPORTANCE_LOW ) applicationContext .getSystemService(NotificationManager::class.java) .createNotificationChannel(channel) } } }
Important:
setProgress() = update app UI progress setForeground() = update notification progress
21. Chaining work
Suppose you want:
Compress image | v Upload image | v Notify server
Use chaining:
val compressWork = OneTimeWorkRequestBuilder<CompressWorker>().build() val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>().build() val notifyWork = OneTimeWorkRequestBuilder<NotifyServerWorker>().build() WorkManager.getInstance(context) .beginWith(compressWork) .then(uploadWork) .then(notifyWork) .enqueue()
Flow:
CompressWorker success | v UploadWorker success | v NotifyServerWorker success
If one worker fails, next worker does not run.
22. Parallel work
Example:
Upload image 1 Upload image 2 Upload image 3 | v Notify server when all uploads complete
val upload1 = OneTimeWorkRequestBuilder<UploadWorker>().build() val upload2 = OneTimeWorkRequestBuilder<UploadWorker>().build() val upload3 = OneTimeWorkRequestBuilder<UploadWorker>().build() val notifyServer = OneTimeWorkRequestBuilder<NotifyServerWorker>().build() WorkManager.getInstance(context) .beginWith(listOf(upload1, upload2, upload3)) .then(notifyServer) .enqueue()
23. Cancel work
Cancel by ID
WorkManager.getInstance(context) .cancelWorkById(uploadRequest.id)
Cancel by tag
WorkManager.getInstance(context) .cancelAllWorkByTag("upload")
Add tag
val uploadRequest = OneTimeWorkRequestBuilder<UploadWorker>() .addTag("upload") .build()
24. Scenario-based examples
Scenario 1: Upload logs when internet is available
Use:
OneTimeWorkRequest + Network constraint + Retry
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val request = OneTimeWorkRequestBuilder<LogUploadWorker>() .setConstraints(constraints) .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS ) .build() WorkManager.getInstance(context).enqueue(request)
Scenario 2: Sync data every 12 hours
Use:
PeriodicWorkRequest
val request = PeriodicWorkRequestBuilder<SyncWorker>( 12, TimeUnit.HOURS ) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() ) .build() WorkManager.getInstance(context) .enqueueUniquePeriodicWork( "app_sync", ExistingPeriodicWorkPolicy.KEEP, request )
Scenario 3: Upload profile photo
Use:
Unique one-time work with REPLACE
WorkManager.getInstance(context) .enqueueUniqueWork( "profile_photo_upload", ExistingWorkPolicy.REPLACE, request )
Why?
If user selects a new profile photo, old upload should cancel and new upload should start.
Scenario 4: Upload report only on Wi-Fi and charging
Use:
Constraints: UNMETERED + Charging
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.UNMETERED) .setRequiresCharging(true) .build()
Scenario 5: Retry failed API call
Use:
Result.retry() + BackoffPolicy
override suspend fun doWork(): Result { return try { api.syncData() Result.success() } catch (e: IOException) { Result.retry() } catch (e: HttpException) { if (e.code() >= 500) { Result.retry() } else { Result.failure() } } }
25. WorkManager vs Service vs AlarmManager
| Requirement | Best API |
|---|---|
| Reliable background sync | WorkManager |
| Retry API call later | WorkManager |
| Upload when network available | WorkManager |
| Periodic sync | WorkManager |
| Music playback | Foreground Service |
| Navigation tracking | Foreground Service |
| Exact alarm at exact time | AlarmManager |
| Immediate UI work | Coroutine/ViewModel |
| Real-time socket | Foreground Service or lifecycle-aware component |
26. WorkManager interview answer
WorkManager is a Jetpack library used for reliable background work. It is best for deferrable tasks that must complete even if the app closes or the device restarts. We define a Worker for the actual task, create a WorkRequest, optionally add constraints like network or charging, and enqueue it using WorkManager. The Worker returns Result.success(), Result.failure(), or Result.retry(). We can observe work state using WorkInfo and update progress using setProgress(). For long-running work, WorkManager can run a foreground worker with a notification using setForeground().
27. Common mistakes
Using WorkManager for exact time alarm Using WorkManager for music playback Forgetting constraints for network work Returning failure for temporary network error Creating duplicate workers instead of unique work Passing large data in inputData Not observing WorkInfo state Using periodic work expecting exact timing
Data is for small key-value input/output. Do not pass large files directly. Pass file URI/path instead.
28. Best memory trick
Worker: "Who will do the work?" WorkRequest: "What work should be done?" Constraints: "When should work run?" WorkManager: "Who manages and schedules the work?" Result: "What happened after work finished?" WorkInfo: "What is current status/progress?"
29. Mini complete example
Worker
class SimpleSyncWorker( context: Context, params: WorkerParameters ) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { return try { delay(2000) Log.d("SimpleSyncWorker", "Sync completed") Result.success() } catch (e: Exception) { Result.retry() } } }
Enqueue
val constraints = Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .build() val request = OneTimeWorkRequestBuilder<SimpleSyncWorker>() .setConstraints(constraints) .addTag("sync") .build() WorkManager.getInstance(context) .enqueue(request)
Observe
WorkManager.getInstance(context) .getWorkInfoByIdLiveData(request.id) .observe(this) { info -> when (info.state) { WorkInfo.State.ENQUEUED -> Log.d("WM", "Waiting") WorkInfo.State.RUNNING -> Log.d("WM", "Running") WorkInfo.State.SUCCEEDED -> Log.d("WM", "Done") WorkInfo.State.FAILED -> Log.d("WM", "Failed") WorkInfo.State.CANCELLED -> Log.d("WM", "Cancelled") else -> Unit } }
Final summary
WorkManager = reliable background task manager. Use it for: - Upload - Sync - Retry - Backup - Periodic background work - Constraint-based work Do not use it for: - Exact alarms - Music playback - Real-time socket - Navigation tracking
Best one-line answer:
WorkManager is used when work should complete reliably, maybe later, even if the app closes, and only when required conditions like network or charging are satisfied.