โš™๏ธ Advanced Android Engineering and Emerging Technology

Background Work with WorkManager

Uploading a photo, syncing data overnight, cleaning old files: work that must finish even if the user leaves the app or the device restarts. WorkManager is the recommended API for this persistent background work. Here's how to use it properly.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. When to use WorkManager
  2. Setup and a CoroutineWorker
  3. Enqueue with constraints
  4. Chaining and observing
  5. Expedited and long-running work
  6. Dependency injection with Hilt
  7. Testing
  8. Use cases
  9. Troubleshooting
  10. Frequently asked questions
  11. Conclusion

Android aggressively limits background execution to save battery. WorkManager works within those limits: it persists scheduled work in a database, respects constraints like network and charging, retries with backoff, and survives app restarts and reboots.

When to use WorkManager

Choosing a background work API Work tied to a screen uses coroutines in viewModelScope. Persistent work that must complete, possibly deferred, uses WorkManager. User-visible ongoing work like media playback or navigation uses a foreground service. Exact-time user-facing events like alarms use AlarmManager. Coroutineswhile screenis active WorkManagermust finish,can be deferred Foreground serviceuser-visible,ongoing now AlarmManagerexact time,user-facing alarm WorkManager is the default for persistent work.
Pick the API by whether work must persist, must run now, and whether the user sees it.

Setup and a CoroutineWorker

build.gradle.ktsGradle KTS
dependencies {
    implementation("androidx.work:work-runtime-ktx:2.10.0") // check for the latest stable version
}
SyncWorker.ktKotlin
class SyncWorker(
    appContext: Context,
    params: WorkerParameters,
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        val since = inputData.getLong(KEY_SINCE, 0L)
        return try {
            val count = syncRepository().syncChangesSince(since)     // suspend, main-safe
            Result.success(workDataOf(KEY_COUNT to count))
        } catch (e: IOException) {
            if (runAttemptCount < 5) Result.retry() else Result.failure()
        }
    }

    companion object {
        const val KEY_SINCE = "since"
        const val KEY_COUNT = "count"
    }
}

Return success, retry (uses backoff) or failure. Treat work as idempotent: it may run more than once.

Enqueue with constraints

Scheduling workKotlin
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)
    .setRequiresBatteryNotLow(true)
    .build()

// One-time, unique: avoid duplicate syncs if the user taps twice
val oneTime = OneTimeWorkRequestBuilder<SyncWorker>()
    .setConstraints(constraints)
    .setInputData(workDataOf(SyncWorker.KEY_SINCE to lastSync))
    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
    .build()
WorkManager.getInstance(context)
    .enqueueUniqueWork("sync", ExistingWorkPolicy.KEEP, oneTime)

// Periodic: minimum interval is 15 minutes
val periodic = PeriodicWorkRequestBuilder<SyncWorker>(12, TimeUnit.HOURS)
    .setConstraints(constraints)
    .build()
WorkManager.getInstance(context)
    .enqueueUniquePeriodicWork("periodic-sync", ExistingPeriodicWorkPolicy.KEEP, periodic)
Timing isn't exact

WorkManager runs work when constraints are met and the system allows. Doze and App Standby buckets can delay it. Don't use it when work must happen at a precise time.

Chaining and observing

Chain and observeKotlin
WorkManager.getInstance(context)
    .beginUniqueWork("upload", ExistingWorkPolicy.REPLACE, compressRequest)
    .then(uploadRequest)          // receives compress output as input
    .then(cleanupRequest)
    .enqueue()

// Observe progress/state as Flow (e.g. in a ViewModel)
val uploadState: Flow<WorkInfo.State?> =
    WorkManager.getInstance(context)
        .getWorkInfosForUniqueWorkFlow("upload")
        .map { infos -> infos.lastOrNull()?.state }

Expedited and long-running work

  • Expedited work (setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)): for short, important tasks the user started, like sending a message. Subject to quotas.
  • Long-running work: call setForeground(...) from the worker to run as a foreground service with a notification, declaring the right foreground service type. See Foreground Services.

Dependency injection with Hilt

HiltWorkerKotlin
@HiltWorker
class SyncWorker @AssistedInject constructor(
    @Assisted appContext: Context,
    @Assisted params: WorkerParameters,
    private val repository: SyncRepository,
) : CoroutineWorker(appContext, params) {
    override suspend fun doWork(): Result = /* ... */ Result.success()
}
// Application implements Configuration.Provider with HiltWorkerFactory,
// and the default WorkManager initializer is removed in the manifest.

Testing

SyncWorkerTest.ktKotlin
@Test
fun syncWorker_succeeds() = runTest {
    val worker = TestListenableWorkerBuilder<SyncWorker>(context)
        .setInputData(workDataOf(SyncWorker.KEY_SINCE to 0L))
        .build()
    val result = worker.doWork()
    assertTrue(result is ListenableWorker.Result.Success)
}

Use cases

Offline-first sync

Push queued local changes when the network returns. See Offline-First Apps.

Media upload

Compress, upload and clean up as a chain, with progress shown in the UI.

Maintenance

Periodic cache cleanup or database pruning while charging.

Troubleshooting

  • Work never runs: check constraints, inspect with Background Task Inspector in Android Studio, and check if the app is force-stopped (force-stop cancels scheduled work until the app is opened again).
  • Duplicate work: use unique work with the right existing-work policy.
  • Hilt worker crashes on creation: the custom WorkManager configuration or initializer removal is missing.

Frequently asked questions

Does WorkManager survive reboots?

Yes. Work is persisted and rescheduled after reboot.

Can periodic work run every minute?

No. The minimum interval is 15 minutes. For real-time updates use push messages (FCM) or a foreground service when appropriate.

Conclusion

Use WorkManager for persistent, deferrable work: write idempotent CoroutineWorkers, add constraints, use unique work to avoid duplicates, chain steps, observe state as Flow, and test with TestListenableWorkerBuilder.

Next step: handle ongoing, user-visible tasks with Foreground Services in Android.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Architecture makes the most sense when you apply it to a real project and see why each boundary exists. Learn how maintainable Android applications are designed by building one.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp