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

ClassMeaning
WorkerPerforms background task synchronously
CoroutineWorkerPerforms background task using Kotlin coroutine
WorkRequestRequest object that describes work
OneTimeWorkRequestWork runs once
PeriodicWorkRequestWork runs repeatedly
ConstraintsConditions like network, charging, battery
WorkManagerEnqueues, observes, cancels work
WorkInfoCurrent state/progress of work
DataInput/output key-value data

6. Worker states

WorkManager work moves through states:

ENQUEUED
   |
   v
RUNNING
   |
   +----> SUCCEEDED
   |
   +----> FAILED
   |
   +----> RETRY / ENQUEUED again
   |
   +----> CANCELLED

Meaning:

StateMeaning
ENQUEUEDWaiting to run
RUNNINGCurrently executing
SUCCEEDEDCompleted successfully
FAILEDFailed permanently
CANCELLEDCancelled by app/user
BLOCKEDWaiting 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

PolicyMeaning
KEEPKeep old work, ignore new one
REPLACECancel old work, start new one
APPENDAdd new work after old work
APPEND_OR_REPLACEAppend 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

RequirementBest API
Reliable background syncWorkManager
Retry API call laterWorkManager
Upload when network availableWorkManager
Periodic syncWorkManager
Music playbackForeground Service
Navigation trackingForeground Service
Exact alarm at exact timeAlarmManager
Immediate UI workCoroutine/ViewModel
Real-time socketForeground 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.