Some tasks need to happen even when the user isn’t looking at your app. Syncing data in the background. Uploading photos. Downloading content for offline use. Cleaning up old cache files. These tasks need to run reliably — even if the user closes the app, even if the phone restarts, even if there’s no internet right now (try again later). That’s what WorkManager does. It’s Android’s recommended solution for deferrable, guaranteed background work. This guide covers it from zero to production.


The Mental Model — When to Use WorkManager

// Think of WorkManager like a POSTAL SERVICE for background tasks:
//
// You drop off a PACKAGE (WorkRequest) at the post office (WorkManager):
// "Please deliver this when there's WiFi and the battery isn't low"
//
// The postal service GUARANTEES delivery:
// ✅ Even if the user closes the app
// ✅ Even if the phone restarts
// ✅ Even if the conditions aren't met right now (waits until they are)
// ✅ Even if the task fails (retries automatically)
//
// You don't need to keep your app running — WorkManager handles it
//
// WHEN to use WorkManager:
// ✅ Sync data to server (upload logs, sync offline changes)
// ✅ Upload images/files in the background
// ✅ Download content for offline use
// ✅ Periodic cleanup (delete old cache, compress images)
// ✅ Send analytics batches
// ✅ Database maintenance (vacuum, migrate)
//
// WHEN NOT to use WorkManager:
// ❌ Immediate user-facing tasks (use coroutines in ViewModel)
// ❌ Real-time updates (use WebSocket or FCM)
// ❌ Exact-time alarms (use AlarmManager)
// ❌ Long-running foreground tasks (use Foreground Service)
//     (Though WorkManager CAN run as foreground — covered later)
//
// The key word is DEFERRABLE:
// "This needs to happen, but it doesn't have to happen RIGHT NOW"

Setup

// build.gradle.kts
dependencies {
    implementation("androidx.work:work-runtime-ktx:2.9.1")
    // work-runtime-ktx — core WorkManager with Kotlin coroutine support
    // Includes CoroutineWorker, suspend support, Flow integration

    // For Hilt integration:
    implementation("androidx.hilt:hilt-work:1.2.0")
    // hilt-work — inject dependencies into Workers
    ksp("androidx.hilt:hilt-compiler:1.2.0")

    // For testing:
    testImplementation("androidx.work:work-testing:2.9.1")
}

Step 1 — Create a Worker

A Worker is a class that defines what work to do. You put your background task logic here:

// The simplest Worker — extends CoroutineWorker for suspend function support

class SyncArticlesWorker(
    appContext: Context,
    // appContext — the APPLICATION context (not Activity — Workers outlive Activities)
    workerParams: WorkerParameters
    // WorkerParameters is a CLASS from WorkManager — metadata about this work
) : CoroutineWorker(appContext, workerParams) {
    // CoroutineWorker is an ABSTRACT CLASS from work-runtime-ktx
    // It runs doWork() in a COROUTINE on Dispatchers.Default
    // You can call suspend functions directly inside doWork()

    override suspend fun doWork(): Result {
        // doWork() is an ABSTRACT SUSPEND FUNCTION — your task logic goes here
        // Called on a background thread (Dispatchers.Default)
        // Must return a Result to tell WorkManager what happened

        return try {
            // Do the actual work
            val api = /* get your API somehow — Hilt shown later */
            val dao = /* get your DAO */

            val articles = api.getArticles()
            dao.upsertArticles(articles.map { it.toEntity() })

            Result.success()
            // Result is a CLASS from WorkManager with three factory functions:
            // Result.success() — work completed successfully
            // Result.failure() — work failed permanently (don't retry)
            // Result.retry()   — work failed temporarily (please retry later)

        } catch (e: CancellationException) {
            throw e   // always re-throw!
        } catch (e: Exception) {
            if (runAttemptCount < 3) {
                // runAttemptCount is a PROPERTY on ListenableWorker
                // How many times this work has been attempted (starts at 0)
                Result.retry()
                // retry() → WorkManager will try again later
                // Backoff policy controls the delay between retries
            } else {
                Result.failure()
                // After 3 attempts, give up
            }
        }
    }
}

// The Worker lifecycle:
// 1. WorkManager decides it's time to run your work
// 2. Creates an instance of your Worker class
// 3. Calls doWork() on a background thread
// 4. doWork() returns Result.success/failure/retry
// 5. Worker instance is DESTROYED (don't store state in the Worker!)
// 6. If retry → new instance created later, doWork() called again

Step 2 — Schedule the Work

One-time work

// Run the work ONCE

val syncRequest = OneTimeWorkRequestBuilder<SyncArticlesWorker>()
    // OneTimeWorkRequestBuilder is a TOP-LEVEL INLINE FUNCTION from work-runtime-ktx
    // Returns OneTimeWorkRequest.Builder — a CLASS for configuring one-time work
    // The type parameter specifies which Worker class to run
    .build()

WorkManager.getInstance(context).enqueue(syncRequest)
// WorkManager is a CLASS from work-runtime
// getInstance() is a STATIC FUNCTION — returns the singleton WorkManager instance
// enqueue() is a FUNCTION on WorkManager — schedules the work
// The work starts ASAP (subject to constraints, if any)

// That's the minimum! Two lines:
// 1. Build the request
// 2. Enqueue it

Periodic work

// Run the work REPEATEDLY on a schedule

val periodicSync = PeriodicWorkRequestBuilder<SyncArticlesWorker>(
    // PeriodicWorkRequestBuilder is a TOP-LEVEL INLINE FUNCTION
    // Returns PeriodicWorkRequest.Builder — a CLASS
    repeatInterval = 1,
    repeatIntervalTimeUnit = TimeUnit.HOURS
    // Runs approximately every 1 hour
    // ⚠️ Minimum interval is 15 MINUTES — anything less is ignored
)
.build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    // enqueueUniquePeriodicWork() is a FUNCTION on WorkManager
    "periodic_article_sync",
    // Unique name — prevents duplicate work
    // If work with this name already exists, the policy decides what to do
    ExistingPeriodicWorkPolicy.KEEP,
    // ExistingPeriodicWorkPolicy is an ENUM from WorkManager:
    // KEEP — keep the existing work, ignore the new request
    // UPDATE — update the existing work with new configuration
    // CANCEL_AND_REENQUEUE — cancel existing, start fresh
    periodicSync
)

// Why KEEP?
// This code might run on every app launch
// Without KEEP: every launch schedules ANOTHER periodic sync
// With KEEP: if already scheduled → does nothing (correct!)

// Flex interval — allow some flexibility in timing:
val flexSync = PeriodicWorkRequestBuilder<SyncArticlesWorker>(
    repeatInterval = 1, repeatIntervalTimeUnit = TimeUnit.HOURS,
    flexInterval = 15, flexIntervalTimeUnit = TimeUnit.MINUTES
    // The work runs sometime in the last 15 minutes of each hour
    // WorkManager can batch with other work → better battery life
).build()

Step 3 — Add Constraints

Constraints tell WorkManager when the work is allowed to run:

val constraints = Constraints.Builder()
    // Constraints is a CLASS from WorkManager
    // Constraints.Builder is a CLASS — builds constraint specifications
    
    .setRequiredNetworkType(NetworkType.CONNECTED)
    // setRequiredNetworkType() is a FUNCTION on Builder
    // NetworkType is an ENUM from WorkManager:
    // NOT_REQUIRED — no network needed (default)
    // CONNECTED — any internet connection
    // UNMETERED — WiFi only (not mobile data)
    // METERED — mobile data only (rare use case)
    // TEMPORARILY_UNMETERED — temporarily unmetered
    
    .setRequiresBatteryNotLow(true)
    // setRequiresBatteryNotLow() is a FUNCTION on Builder
    // Only run when battery is above ~20%
    // Prevents draining the user's battery with background work
    
    .setRequiresCharging(false)
    // setRequiresCharging() is a FUNCTION on Builder
    // true = only run while plugged in (good for heavy work like database cleanup)
    
    .setRequiresStorageNotLow(true)
    // setRequiresStorageNotLow() is a FUNCTION on Builder
    // Only run when storage isn't critically low
    
    .setRequiresDeviceIdle(false)
    // setRequiresDeviceIdle() is a FUNCTION on Builder
    // true = only run when device is idle (screen off, not used recently)
    // Good for heavy maintenance tasks
    
    .build()

// Apply constraints to work request:
val syncRequest = OneTimeWorkRequestBuilder<SyncArticlesWorker>()
    .setConstraints(constraints)
    // setConstraints() is a FUNCTION on WorkRequest.Builder
    .build()

// How constraints work:
// 1. You enqueue the work → WorkManager stores it
// 2. WorkManager checks constraints:
//    - Network available? ✅
//    - Battery not low? ✅
//    - All constraints met → runs the work
// 3. If constraints NOT met → WorkManager WAITS
//    - User goes to WiFi → constraint met → work runs
//    - Battery charges above 20% → constraint met → work runs
// 4. If constraints STOP being met during work → work is STOPPED and retried later

Step 4 — Input and Output Data

// Pass data TO a Worker and get data BACK from it

// SENDING data to Worker:
val inputData = workDataOf(
    // workDataOf() is a TOP-LEVEL FUNCTION from work-runtime-ktx
    // Creates a Data object (like a small Bundle) with key-value pairs
    "article_id" to "abc-123",
    "force_refresh" to true
)
// Data is a CLASS from WorkManager — holds key-value pairs
// ⚠️ Max size: 10 KB — for small data only (IDs, flags, short strings)
// For large data: save to a file/database, pass the file path or ID

val uploadRequest = OneTimeWorkRequestBuilder<UploadArticleWorker>()
    .setInputData(inputData)
    // setInputData() is a FUNCTION on WorkRequest.Builder
    .build()

// RECEIVING data in Worker:
class UploadArticleWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        val articleId = inputData.getString("article_id")
        // inputData is a PROPERTY on ListenableWorker — the Data passed via setInputData
        // getString() is a FUNCTION on Data — reads a String value by key
        // Also: getInt(), getLong(), getBoolean(), getStringArray(), etc.

        val forceRefresh = inputData.getBoolean("force_refresh", false)
        // Second param is default value if key doesn't exist

        // Do work with articleId...

        // RETURNING data from Worker:
        val outputData = workDataOf(
            "upload_url" to "https://example.com/articles/abc-123",
            "upload_time" to System.currentTimeMillis()
        )
        return Result.success(outputData)
        // Result.success(Data) — pass data back to the caller
        // The output can be observed via WorkInfo (shown later)
    }
}

Step 5 — Chaining Work

Run multiple Workers in sequence — the output of one becomes the input of the next:

// Chain: Download → Process → Upload
//
//  Download ──→ Process ──→ Upload
//  (fetch data)  (transform)  (send result)

val download = OneTimeWorkRequestBuilder<DownloadWorker>().build()
val process = OneTimeWorkRequestBuilder<ProcessWorker>().build()
val upload = OneTimeWorkRequestBuilder<UploadWorker>().build()

WorkManager.getInstance(context)
    .beginWith(download)
    // beginWith() is a FUNCTION on WorkManager — starts a chain
    // Returns WorkContinuation — a CLASS for building chains
    .then(process)
    // then() is a FUNCTION on WorkContinuation — adds the next Worker
    // ProcessWorker runs ONLY after DownloadWorker succeeds
    // ProcessWorker can access DownloadWorker's output data
    .then(upload)
    // UploadWorker runs after ProcessWorker succeeds
    .enqueue()
    // enqueue() is a FUNCTION on WorkContinuation — schedules the entire chain

// If DownloadWorker fails → ProcessWorker and UploadWorker are CANCELLED
// If DownloadWorker retries → chain waits until it succeeds

// PARALLEL work → then sequential:
//
//  FetchArticles ─┐
//                  ├──→ MergeResults ──→ Upload
//  FetchComments ─┘
//
val fetchArticles = OneTimeWorkRequestBuilder<FetchArticlesWorker>().build()
val fetchComments = OneTimeWorkRequestBuilder<FetchCommentsWorker>().build()
val merge = OneTimeWorkRequestBuilder<MergeWorker>().build()
val upload = OneTimeWorkRequestBuilder<UploadWorker>().build()

WorkManager.getInstance(context)
    .beginWith(listOf(fetchArticles, fetchComments))
    // beginWith(List) — runs BOTH in parallel
    .then(merge)
    // merge runs after BOTH parallel workers succeed
    .then(upload)
    .enqueue()

Step 6 — Observe Work Status

// Track whether work is running, succeeded, or failed

// Observe by work request ID:
val workManager = WorkManager.getInstance(context)
val workRequest = OneTimeWorkRequestBuilder<SyncArticlesWorker>().build()
workManager.enqueue(workRequest)

// In ViewModel — observe as Flow:
val syncStatus: Flow<WorkInfo?> = workManager
    .getWorkInfoByIdFlow(workRequest.id)
    // getWorkInfoByIdFlow() is a FUNCTION on WorkManager
    // Returns Flow<WorkInfo?>
    // WorkInfo is a CLASS — holds the current state of the work

// Or observe unique work by name:
val syncStatus: Flow<List<WorkInfo>> = workManager
    .getWorkInfosForUniqueWorkFlow("periodic_article_sync")
    // getWorkInfosForUniqueWorkFlow() is a FUNCTION on WorkManager
    // Returns Flow of ALL work infos for this unique name

// In Compose:
@Composable
fun SyncButton(viewModel: SyncViewModel = hiltViewModel()) {
    val workInfo by viewModel.syncStatus.collectAsStateWithLifecycle(initialValue = null)

    val isSyncing = workInfo?.state == WorkInfo.State.RUNNING
    // WorkInfo.State is an ENUM with values:
    // ENQUEUED — waiting to run (constraints not met, or queued)
    // RUNNING — currently executing
    // SUCCEEDED — completed successfully
    // FAILED — failed permanently
    // BLOCKED — waiting for prerequisite work in a chain
    // CANCELLED — cancelled by the app

    Button(
        onClick = { viewModel.startSync() },
        enabled = !isSyncing
    ) {
        if (isSyncing) {
            CircularProgressIndicator(modifier = Modifier.size(16.dp))
            Spacer(Modifier.width(8.dp))
            Text("Syncing...")
        } else {
            Text("Sync Now")
        }
    }

    // Show result
    when (workInfo?.state) {
        WorkInfo.State.SUCCEEDED -> {
            val uploadUrl = workInfo?.outputData?.getString("upload_url")
            // outputData is a PROPERTY on WorkInfo — the Data returned by Result.success(data)
            Text("Sync complete!")
        }
        WorkInfo.State.FAILED -> Text("Sync failed")
        else -> { /* enqueued, running, etc. */ }
    }
}

Step 7 — Retry and Backoff Policy

// When doWork() returns Result.retry(), WorkManager waits before trying again
// The BACKOFF POLICY controls how long to wait

val syncRequest = OneTimeWorkRequestBuilder<SyncArticlesWorker>()
    .setBackoffCriteria(
        // setBackoffCriteria() is a FUNCTION on WorkRequest.Builder
        BackoffPolicy.EXPONENTIAL,
        // BackoffPolicy is an ENUM from WorkManager:
        // LINEAR — wait 10s, then 20s, then 30s, then 40s...
        // EXPONENTIAL — wait 10s, then 20s, then 40s, then 80s...
        // Exponential is usually better — gives the server time to recover
        
        10, TimeUnit.SECONDS
        // Initial delay before first retry
        // ⚠️ Minimum is 10 seconds — anything less is ignored
        // Maximum is 5 hours — caps at 18000 seconds
    )
    .build()

// Retry timeline with EXPONENTIAL, 10s initial:
// Attempt 1: runs immediately → fails → returns retry()
// Attempt 2: waits 10s → runs → fails → returns retry()
// Attempt 3: waits 20s → runs → fails → returns retry()
// Attempt 4: waits 40s → runs → fails → returns retry()
// Attempt 5: waits 80s → runs → succeeds ✅
//
// In doWork(), you control WHEN to retry vs fail permanently:
override suspend fun doWork(): Result {
    return try {
        // do work...
        Result.success()
    } catch (e: IOException) {
        Result.retry()     // network error → temporary, retry makes sense
    } catch (e: HttpException) {
        if (e.code() in 500..599) {
            Result.retry() // server error → temporary, retry
        } else {
            Result.failure() // 4xx client error → permanent, don't retry
        }
    }
}

Step 8 — Long-Running Work (Foreground Service)

// Android limits background work to ~10 minutes
// For longer tasks (large file upload, extended sync), use FOREGROUND work
// This shows a notification and keeps the work running

class LargeUploadWorker(
    appContext: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        // Tell WorkManager to run this as a foreground service
        setForeground(createForegroundInfo("Uploading..."))
        // setForeground() is a SUSPEND FUNCTION on CoroutineWorker
        // Shows a NOTIFICATION and runs as a foreground service
        // The work won't be killed by the system

        // Do the long-running work
        val totalFiles = 100
        for (i in 0 until totalFiles) {
            uploadFile(i)

            // Update the notification with progress
            setForeground(createForegroundInfo("Uploading ${i + 1}/$totalFiles"))
            // Each call updates the notification text
        }

        return Result.success()
    }

    private fun createForegroundInfo(progress: String): ForegroundInfo {
        // ForegroundInfo is a CLASS from WorkManager
        // Wraps a notification that's shown while work runs in foreground

        val notification = NotificationCompat.Builder(applicationContext, "upload_channel")
            .setContentTitle("Article Upload")
            .setContentText(progress)
            .setSmallIcon(R.drawable.ic_upload)
            .setOngoing(true)
            // setOngoing(true) — user can't dismiss the notification
            .build()

        return ForegroundInfo(
            1001,                              // notification ID
            notification,
            ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
            // FOREGROUND_SERVICE_TYPE_DATA_SYNC — declares the type of foreground work
            // Required on Android 14+ — must also be declared in AndroidManifest.xml:
            // <service android:name="androidx.work.impl.foreground.SystemForegroundService"
            //     android:foregroundServiceType="dataSync" />
        )
    }
}

Hilt Integration — Injecting Dependencies into Workers

// Workers are NOT created by Hilt by default — they're created by WorkManager
// You need @HiltWorker to inject dependencies

@HiltWorker
// @HiltWorker is an ANNOTATION from hilt-work
// Tells Hilt: "inject dependencies into this Worker's constructor"
class SyncArticlesWorker @AssistedInject constructor(
    // @AssistedInject is an ANNOTATION from Dagger — for assisted injection
    // Context and WorkerParameters are provided by WorkManager (assisted)
    // Other dependencies are provided by Hilt (injected)
    @Assisted appContext: Context,
    // @Assisted is an ANNOTATION — marks parameters provided by the framework, not Hilt
    @Assisted workerParams: WorkerParameters,
    private val api: ArticleApi,              // ← injected by Hilt
    private val dao: ArticleDao               // ← injected by Hilt
) : CoroutineWorker(appContext, workerParams) {

    override suspend fun doWork(): Result {
        return try {
            val articles = api.getArticles()
            dao.upsertArticles(articles.map { it.toEntity() })
            Result.success()
        } catch (e: CancellationException) { throw e }
        catch (e: Exception) {
            Result.retry()
        }
    }
}

// Setup Hilt's WorkerFactory in your Application class:
@HiltAndroidApp
class MyApplication : Application(), Configuration.Provider {
    // Configuration.Provider is an INTERFACE from WorkManager
    // Provides custom WorkManager configuration

    @Inject lateinit var workerFactory: HiltWorkerFactory
    // HiltWorkerFactory is a CLASS from hilt-work
    // Creates Workers with Hilt injection

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            // Configuration.Builder is a CLASS from WorkManager
            .setWorkerFactory(workerFactory)
            // setWorkerFactory() is a FUNCTION on Builder
            // Tells WorkManager: "use Hilt to create Workers"
            .build()
}

// In AndroidManifest.xml — disable default WorkManager initialization:
// <provider
//     android:name="androidx.startup.InitializationProvider"
//     android:authorities="${applicationId}.androidx-startup"
//     tools:node="merge">
//     <meta-data
//         android:name="androidx.work.WorkManagerInitializer"
//         android:value="androidx.startup"
//         tools:node="remove" />
// </provider>
//
// This prevents WorkManager from initializing before Hilt is ready

Cancelling Work

val workManager = WorkManager.getInstance(context)

// Cancel by unique name:
workManager.cancelUniqueWork("periodic_article_sync")
// cancelUniqueWork() is a FUNCTION on WorkManager

// Cancel by ID:
workManager.cancelWorkById(workRequest.id)
// cancelWorkById() is a FUNCTION on WorkManager

// Cancel by tag:
val taggedRequest = OneTimeWorkRequestBuilder<SyncArticlesWorker>()
    .addTag("sync")
    // addTag() is a FUNCTION on WorkRequest.Builder — adds a searchable tag
    .build()
workManager.cancelAllWorkByTag("sync")
// cancelAllWorkByTag() is a FUNCTION on WorkManager

// Cancel ALL work:
workManager.cancelAllWork()
// ⚠️ Cancels everything — use carefully

// When work is cancelled:
// - doWork() coroutine is cancelled (CancellationException)
// - WorkInfo.State becomes CANCELLED
// - If part of a chain, dependent work is also cancelled

Common Mistakes to Avoid

Mistake 1: Using WorkManager for immediate tasks

// ❌ User taps "Save" → schedule WorkManager to save
// WorkManager might delay execution → user sees nothing happened!

// ✅ Use coroutines for immediate work
viewModelScope.launch {
    repository.saveArticle(article)   // immediate, in the ViewModel
}

// ✅ Use WorkManager for DEFERRABLE work
// "Sync this to the server later, even if the app is closed"
WorkManager.getInstance(context).enqueue(syncRequest)

Mistake 2: Not using unique work for periodic tasks

// ❌ enqueue() on every app launch → stacks up duplicate periodic work!
WorkManager.getInstance(context).enqueue(periodicRequest)
// First launch: schedules periodic sync ✅
// Second launch: schedules ANOTHER periodic sync ❌ (now two running!)
// Third launch: THREE running! ❌❌❌

// ✅ Use enqueueUniquePeriodicWork with KEEP policy
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "sync", ExistingPeriodicWorkPolicy.KEEP, periodicRequest
)
// First launch: schedules ✅
// Second launch: "sync" already exists → KEEP → does nothing ✅

Mistake 3: Storing large data in inputData/outputData

// ❌ Data has a 10 KB limit — large JSON will crash
val data = workDataOf("articles" to hugeJsonString)   // 💥 over 10 KB!

// ✅ Save large data to file/database, pass the reference
val data = workDataOf("article_id" to "abc-123")   // just an ID
// Worker reads the full data from the database using the ID

Mistake 4: Not declaring foreground service type on Android 14+

// ❌ setForeground() without manifest declaration → crash on Android 14+

// ✅ Declare in AndroidManifest.xml:
// <service
//     android:name="androidx.work.impl.foreground.SystemForegroundService"
//     android:foregroundServiceType="dataSync"
//     android:exported="false" />

// And request the permission:
// <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />

Mistake 5: Ignoring CancellationException in Workers

// ❌ Catching all exceptions including CancellationException
override suspend fun doWork(): Result {
    return try {
        // do work
        Result.success()
    } catch (e: Exception) {   // catches CancellationException too!
        Result.retry()          // retries a cancelled Worker — wastes resources
    }
}

// ✅ Always re-throw CancellationException
override suspend fun doWork(): Result {
    return try {
        Result.success()
    } catch (e: CancellationException) {
        throw e   // let the cancellation happen
    } catch (e: Exception) {
        Result.retry()
    }
}

Summary

  • WorkManager (class) is Android’s solution for deferrable, guaranteed background work — survives app close, device restart
  • CoroutineWorker (abstract class) is the Kotlin-friendly Worker — implement doWork() as a suspend function
  • Result (class) has three outcomes: success(), failure(), retry()
  • OneTimeWorkRequestBuilder<T>() (top-level inline function) creates a one-time work request
  • PeriodicWorkRequestBuilder<T>() (top-level inline function) creates periodic work (min 15 minutes)
  • Constraints (class) specify when work can run: network type, battery, charging, storage, device idle
  • NetworkType (enum): NOT_REQUIRED, CONNECTED, UNMETERED, METERED
  • enqueueUniquePeriodicWork() (function on WorkManager) prevents duplicate periodic work — use ExistingPeriodicWorkPolicy.KEEP
  • workDataOf() (top-level function) creates Data for input/output — max 10 KB
  • Chain work with beginWith().then().enqueue() — sequential or parallel execution
  • Observe status with getWorkInfoByIdFlow() (function on WorkManager) — returns Flow<WorkInfo>
  • WorkInfo.State (enum): ENQUEUED, RUNNING, SUCCEEDED, FAILED, BLOCKED, CANCELLED
  • BackoffPolicy (enum): LINEAR or EXPONENTIAL — controls retry delay
  • setForeground() (suspend function on CoroutineWorker) runs work as foreground service with notification
  • ForegroundInfo (class) wraps a notification + foreground service type
  • @HiltWorker (annotation) + @AssistedInject enables dependency injection into Workers
  • HiltWorkerFactory (class) creates Workers with Hilt — register in Application’s Configuration
  • Cancel with cancelUniqueWork(), cancelWorkById(), or cancelAllWorkByTag()
  • Always re-throw CancellationException in doWork()

WorkManager is the right tool when work needs to happen reliably in the background — even if the app is closed or the phone restarts. Sync data, upload files, clean caches, send analytics — schedule it with constraints, let WorkManager handle the rest. Add Hilt for dependency injection, use chains for multi-step workflows, observe progress with Flow, and your background work is production-ready.

Happy coding!