DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at myougatheaxo.gumroad.com

WorkManager in Android: Background Tasks That Actually Work

WorkManager in Android: Background Tasks That Actually Work

When developing Android applications, background tasks are essential for many use cases: syncing data, sending notifications, uploading files, or running periodic checks. However, implementing reliable background task handling can be tricky, especially with Android's varying versions and power management policies.

This is where WorkManager comes in—Google's recommended solution for deferrable background work on Android.

What is WorkManager?

WorkManager is part of Android Jetpack and provides a backwards-compatible way to execute background work that is guaranteed to execute, even if the app is closed or the device restarts. Unlike Handler, Thread, or deprecated IntentService, WorkManager respects system constraints and user preferences.

Key Features:

  • Guaranteed execution: Tasks persist across app restarts
  • Backwards compatible: Works on API 14+
  • Power-aware: Respects Doze mode and battery optimization
  • Flexible scheduling: One-time or periodic execution
  • Observable: LiveData/Flow for status monitoring
  • Chainable: Execute dependent tasks in sequence

OneTimeWorkRequest: Single Tasks

For one-off background tasks that should execute once, use OneTimeWorkRequest:

import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import android.content.Context

class UploadDataWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        return try {
            // Your background work here
            val data = fetchLocalData()
            uploadToServer(data)

            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }

    private fun fetchLocalData(): String {
        return "sample_data"
    }

    private fun uploadToServer(data: String) {
        // Simulate API call
        println("Uploading: $data")
    }
}

// Schedule the work
val uploadWork = OneTimeWorkRequestBuilder<UploadDataWorker>()
    .build()

WorkManager.getInstance(context).enqueueUniqueWork(
    "upload_data",
    ExistingWorkPolicy.KEEP,
    uploadWork
)
Enter fullscreen mode Exit fullscreen mode

Key points:

  • Return Result.success() for successful completion
  • Return Result.retry() to retry (with exponential backoff)
  • Return Result.failure() to stop and not retry
  • WorkManager automatically retries with exponential backoff (15 sec, 30 sec, 1 hour, etc.)

PeriodicWorkRequest: Repeating Tasks

For recurring tasks like syncing data or checking for updates:

import androidx.work.PeriodicWorkRequestBuilder
import java.util.concurrent.TimeUnit

class SyncDataWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        return try {
            syncWithServer()
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }

    private fun syncWithServer() {
        println("Syncing data with server...")
    }
}

// Schedule periodic work - minimum interval is 15 minutes
val syncWork = PeriodicWorkRequestBuilder<SyncDataWorker>(
    15, TimeUnit.MINUTES
).build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "sync_data",
    ExistingPeriodicWorkPolicy.KEEP,
    syncWork
)
Enter fullscreen mode Exit fullscreen mode

Important: The minimum interval for periodic work is 15 minutes. Shorter intervals will be adjusted by WorkManager.

Constraints: Conditional Execution

Control when tasks should run with constraints:

import androidx.work.Constraints
import androidx.work.NetworkType

// Create constraints
val constraints = Constraints.Builder()
    .setRequiredNetworkType(NetworkType.CONNECTED)  // Only run when connected
    .setRequiresCharging(false)                      // Works on battery
    .setRequiresBatteryNotLow(true)                  // Don't run if battery low
    .setRequiresStorageNotLow(true)                  // Ensure storage space
    .setRequiresDeviceIdle(false)                    // Can run even when device is in use
    .build()

// Apply constraints to work
val uploadWork = OneTimeWorkRequestBuilder<UploadDataWorker>()
    .setConstraints(constraints)
    .build()

WorkManager.getInstance(context).enqueue(uploadWork)
Enter fullscreen mode Exit fullscreen mode

Common constraints:

  • setRequiredNetworkType(): CONNECTED, METERED, UNMETERED, NOT_REQUIRED
  • setRequiresCharging(): Run only while charging
  • setRequiresBatteryNotLow(): Skip if battery is low
  • setRequiresStorageNotLow(): Ensure sufficient storage
  • setRequiresDeviceIdle(): Run only when Doze is inactive

Chaining: Dependent Work

Execute tasks in sequence, passing data between them:

import androidx.work.OneTimeWorkRequest

class WorkerA(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        val output = Data.Builder()
            .putString("key", "result_from_a")
            .build()
        return Result.success(output)
    }
}

class WorkerB(context: Context, params: WorkerParameters) : Worker(context, params) {
    override fun doWork(): Result {
        val input = inputData.getString("key") ?: ""
        println("Received from A: $input")
        return Result.success()
    }
}

// Chain them
val workA = OneTimeWorkRequestBuilder<WorkerA>().build()
val workB = OneTimeWorkRequestBuilder<WorkerB>().build()

WorkManager.getInstance(context)
    .beginWith(workA)
    .then(workB)
    .enqueue()
Enter fullscreen mode Exit fullscreen mode

You can also use beginUniqueWork() and enqueueUniqueWork() to prevent duplicate chains:

WorkManager.getInstance(context)
    .beginUniqueWork("data_sync", ExistingWorkPolicy.KEEP, workA)
    .then(workB)
    .enqueue()
Enter fullscreen mode Exit fullscreen mode

Observing Work Status

Monitor your work's progress and status:

// Observe single work
WorkManager.getInstance(context)
    .getWorkInfoByIdLiveData(uploadWork.id)
    .observe(lifecycleOwner) { workInfo ->
        when {
            workInfo?.state == WorkInfo.State.SUCCEEDED -> {
                println("Work succeeded!")
            }
            workInfo?.state == WorkInfo.State.FAILED -> {
                println("Work failed!")
            }
            workInfo?.state == WorkInfo.State.RUNNING -> {
                println("Work in progress...")
            }
        }
    }

// Or observe by tag
WorkManager.getInstance(context)
    .getWorkInfosByTagLiveData("my_tag")
    .observe(lifecycleOwner) { workInfoList ->
        workInfoList?.forEach { workInfo ->
            println("Work ${workInfo.id}: ${workInfo.state}")
        }
    }
Enter fullscreen mode Exit fullscreen mode

WorkInfo states:

  • ENQUEUED: Waiting to execute
  • RUNNING: Currently executing
  • SUCCEEDED: Completed successfully
  • FAILED: Failed and won't retry
  • BLOCKED: Dependent work failed
  • CANCELLED: Manually cancelled

Best Practices

  1. Keep workers short-lived: Complete work in seconds, not minutes
  2. Handle exceptions gracefully: Always return appropriate Result
  3. Use constraints wisely: Reduce battery drain by being smart about conditions
  4. Monitor with logging: Observe work status in production
  5. Test on real devices: Emulator behavior differs from actual devices
  6. Use unique work names: Prevent duplicate task scheduling
  7. Cancel work when needed: Call WorkManager.getInstance(context).cancelUniqueWork()

Conclusion

WorkManager is the backbone of reliable background task handling on Android. By understanding OneTimeWorkRequest, PeriodicWorkRequest, constraints, and chaining, you can build robust apps that perform work reliably, even in challenging conditions.

My 8 Android templates include background task patterns. https://myougatheax.gumroad.com

Top comments (0)