Skip to main content

Kotlin WorkManager

Introduction

WorkManager is one of the Android Jetpack architecture components that makes it easier to schedule deferrable, asynchronous tasks that are expected to run even if the application exits or the device restarts. WorkManager is the recommended solution for persistent work, particularly when the work needs to be guaranteed to run even if the app is closed or the device is restarted.

Some key benefits of WorkManager include:

  • Work constraints: Run tasks only when specific conditions are met (like network availability or device charging)
  • Guaranteed execution: Tasks will execute even after app restarts or device reboots
  • Backwards compatibility: Works consistently across different Android versions
  • Battery-friendly: Respects battery optimization features of newer Android versions
  • Chaining: Allows you to create and organize complex task sequences

In this tutorial, we'll explore how to implement WorkManager in a Kotlin Android application, covering both simple and more complex use cases.

Setup

To start using WorkManager in your Kotlin Android project, you need to add the required dependencies in your app-level build.gradle file:

groovy
dependencies {
// WorkManager kotlin support
implementation "androidx.work:work-runtime-ktx:2.8.1"
}

Basic WorkManager Implementation

1. Creating a Worker

The first step to implementing WorkManager is to create a Worker class. This class will contain the code that needs to be executed in the background.

kotlin
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf

class SimpleWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

override fun doWork(): Result {
// Get input data
val message = inputData.getString("message") ?: "Default Message"

try {
// Simulate work by sleeping
Log.d("SimpleWorker", "Starting work with message: $message")
for (i in 1..10) {
Log.d("SimpleWorker", "Working... $i/10")
Thread.sleep(1000)
}
Log.d("SimpleWorker", "Work completed successfully!")

// Create output data
val outputData = workDataOf("result" to "Task completed successfully!")

// Return success result with output data
return Result.success(outputData)
} catch (e: Exception) {
Log.e("SimpleWorker", "Error completing work", e)
return Result.failure()
}
}
}

2. Scheduling the Work

Now that we have defined our Worker, we need to create a WorkRequest and enqueue it with the WorkManager.

kotlin
import androidx.work.*
import java.util.concurrent.TimeUnit

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

findViewById<Button>(R.id.btnStartWork).setOnClickListener {
scheduleSimpleWork()
}
}

private fun scheduleSimpleWork() {
// Create input data
val inputData = workDataOf("message" to "Hello from MainActivity")

// Create a OneTimeWorkRequest
val workRequest = OneTimeWorkRequestBuilder<SimpleWorker>()
.setInputData(inputData)
.setBackoffCriteria(
BackoffPolicy.LINEAR,
OneTimeWorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.build()

// Enqueue the work request
WorkManager.getInstance(applicationContext).enqueue(workRequest)

// Observe work status
WorkManager.getInstance(applicationContext)
.getWorkInfoByIdLiveData(workRequest.id)
.observe(this) { workInfo ->
if (workInfo != null) {
val status = workInfo.state
val tvStatus = findViewById<TextView>(R.id.tvStatus)
tvStatus.text = "Status: $status"

// If work is finished, get the output data
if (workInfo.state == WorkInfo.State.SUCCEEDED) {
val result = workInfo.outputData.getString("result")
tvStatus.text = "Status: SUCCEEDED\nResult: $result"
}
}
}
}
}

Work Constraints

One of the powerful features of WorkManager is the ability to set constraints on when your work should run. For example, you might want your work to only run when the device is charging and connected to WiFi.

kotlin
private fun scheduleConstrainedWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // WiFi connection
.setRequiresCharging(true)
.setRequiresBatteryNotLow(true)
.build()

val workRequest = OneTimeWorkRequestBuilder<DataSyncWorker>()
.setConstraints(constraints)
.build()

WorkManager.getInstance(applicationContext).enqueue(workRequest)
}

In this example, the work will only run when:

  • The device is connected to an unmetered network (WiFi)
  • The device is charging
  • The battery is not low

Periodic Work

For tasks that need to be repeated regularly, WorkManager provides PeriodicWorkRequest.

kotlin
private fun schedulePeriodicWork() {
// Minimum interval is 15 minutes
val workRequest = PeriodicWorkRequestBuilder<DataSyncWorker>(
repeatInterval = 1,
repeatIntervalTimeUnit = TimeUnit.HOURS
).build()

// Enqueue with ExistingPeriodicWorkPolicy to handle cases where
// a periodic work with the same name is already enqueued
WorkManager.getInstance(applicationContext)
.enqueueUniquePeriodicWork(
"periodic_data_sync",
ExistingPeriodicWorkPolicy.KEEP, // or REPLACE if you want to replace existing
workRequest
)
}

Note: The minimum interval for periodic work is 15 minutes, and the system might delay the execution to optimize battery life.

Work Chaining

You can chain multiple workers together to create sequences of work. This is useful when one task depends on the output of another.

kotlin
private fun chainedWork() {
val uploadWorkRequest = OneTimeWorkRequestBuilder<UploadWorker>().build()
val processWorkRequest = OneTimeWorkRequestBuilder<ProcessingWorker>().build()
val compressWorkRequest = OneTimeWorkRequestBuilder<CompressionWorker>().build()

WorkManager.getInstance(applicationContext)
.beginWith(compressWorkRequest)
.then(uploadWorkRequest)
.then(processWorkRequest)
.enqueue()
}

In this chain:

  1. First, the CompressionWorker will run
  2. When it completes successfully, the UploadWorker will run
  3. When that completes successfully, the ProcessingWorker will run

You can also run multiple workers in parallel and then continue with another worker when they all complete:

kotlin
private fun parallelWork() {
val downloadWork1 = OneTimeWorkRequestBuilder<DownloadWorker>()
.setInputData(workDataOf("url" to "https://example.com/file1"))
.build()

val downloadWork2 = OneTimeWorkRequestBuilder<DownloadWorker>()
.setInputData(workDataOf("url" to "https://example.com/file2"))
.build()

val processAllRequest = OneTimeWorkRequestBuilder<ProcessAllWorker>().build()

WorkManager.getInstance(applicationContext)
.beginWith(listOf(downloadWork1, downloadWork2))
.then(processAllRequest)
.enqueue()
}

Handling Work Data

WorkManager allows you to pass data between your app and workers, and between chained workers.

Passing Data to a Worker

kotlin
// Create the input data
val inputData = workDataOf(
"key1" to "value1",
"key2" to 42,
"key3" to true
)

// Create the work request with the input data
val workRequest = OneTimeWorkRequestBuilder<MyWorker>()
.setInputData(inputData)
.build()

Receiving Data in a Worker

kotlin
override fun doWork(): Result {
val value1 = inputData.getString("key1") ?: ""
val value2 = inputData.getInt("key2", 0)
val value3 = inputData.getBoolean("key3", false)

// Use the values...

return Result.success()
}

Returning Data from a Worker

kotlin
override fun doWork(): Result {
// Do the work...

// Create output data
val outputData = workDataOf(
"result" to "Work completed",
"timestamp" to System.currentTimeMillis()
)

return Result.success(outputData)
}

Real-world Example: Image Processing and Upload

Let's create a more complex, real-world example. We'll develop an app that processes images (blurring) and then uploads them to a server. We'll break this down into multiple workers:

  1. BlurImageWorker: Takes an image URI, applies a blur filter, and saves the result
  2. UploadImageWorker: Takes a processed image and uploads it to a server

BlurImageWorker Implementation

kotlin
class BlurImageWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

override fun doWork(): Result {
val imageUriString = inputData.getString("image_uri")
if (imageUriString.isNullOrEmpty()) {
return Result.failure()
}

return try {
val imageUri = Uri.parse(imageUriString)
val bitmap = BitmapFactory.decodeStream(
applicationContext.contentResolver.openInputStream(imageUri)
)

// Apply blur algorithm (simplified example)
val blurredBitmap = applyBlur(bitmap, 25)

// Save blurred image to cache directory
val outputFile = File(
applicationContext.cacheDir,
"blurred_${System.currentTimeMillis()}.jpg"
)

FileOutputStream(outputFile).use { out ->
blurredBitmap.compress(Bitmap.CompressFormat.JPEG, 90, out)
}

// Create output data with the URI of the blurred image
val outputData = workDataOf(
"blurred_image_path" to outputFile.absolutePath
)

Result.success(outputData)
} catch (e: Exception) {
Log.e("BlurImageWorker", "Error processing image", e)
Result.failure()
}
}

private fun applyBlur(bitmap: Bitmap, radius: Int): Bitmap {
// Simplified blur implementation - in real app you'd want a proper
// blur algorithm like Renderscript or similar
val output = Bitmap.createBitmap(
bitmap.width, bitmap.height, bitmap.config
)
val renderScript = RenderScript.create(applicationContext)
val bitmapInput = Allocation.createFromBitmap(renderScript, bitmap)
val bitmapOutput = Allocation.createFromBitmap(renderScript, output)

// Apply a blur filter
val blurScript = ScriptIntrinsicBlur.create(renderScript, Element.U8_4(renderScript))
blurScript.setInput(bitmapInput)
blurScript.setRadius(radius.toFloat())
blurScript.forEach(bitmapOutput)

bitmapOutput.copyTo(output)
renderScript.destroy()

return output
}
}

UploadImageWorker Implementation

kotlin
class UploadImageWorker(context: Context, params: WorkerParameters) : Worker(context, params) {

override fun doWork(): Result {
val imagePath = inputData.getString("blurred_image_path")
if (imagePath.isNullOrEmpty()) {
return Result.failure()
}

return try {
// In a real app, you would implement actual upload logic here
// This is a simulated upload that just delays for a few seconds
Log.d("UploadImageWorker", "Starting upload of $imagePath")

// Simulate network delay
for (i in 1..5) {
Log.d("UploadImageWorker", "Uploading... ${i*20}%")
Thread.sleep(1000)
}

// Create mock server response
val serverImageUrl = "https://example.com/images/${System.currentTimeMillis()}"
Log.d("UploadImageWorker", "Upload complete! URL: $serverImageUrl")

// Return success with the server URL
val outputData = workDataOf(
"upload_url" to serverImageUrl,
"upload_timestamp" to System.currentTimeMillis()
)

Result.success(outputData)
} catch (e: Exception) {
Log.e("UploadImageWorker", "Error uploading image", e)
Result.failure()
}
}
}

Chain the Workers Together in Activity

kotlin
class ImageProcessingActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_image_processing)

// Image picker button
findViewById<Button>(R.id.btnPickImage).setOnClickListener {
pickImage()
}
}

private val pickImageLauncher = registerForActivityResult(
ActivityResultContracts.GetContent()
) { uri ->
uri?.let { processAndUploadImage(it) }
}

private fun pickImage() {
pickImageLauncher.launch("image/*")
}

private fun processAndUploadImage(imageUri: Uri) {
// Create input data with image URI
val blurInputData = workDataOf("image_uri" to imageUri.toString())

// Create work requests
val blurRequest = OneTimeWorkRequestBuilder<BlurImageWorker>()
.setInputData(blurInputData)
.build()

val uploadRequest = OneTimeWorkRequestBuilder<UploadImageWorker>()
.build()

// Create and observe work chain
WorkManager.getInstance(applicationContext)
.beginWith(blurRequest)
.then(uploadRequest)
.enqueue()

// Observe the final work
WorkManager.getInstance(applicationContext)
.getWorkInfoByIdLiveData(uploadRequest.id)
.observe(this) { workInfo ->
if (workInfo != null && workInfo.state == WorkInfo.State.SUCCEEDED) {
val uploadUrl = workInfo.outputData.getString("upload_url")
val timestamp = workInfo.outputData.getLong("upload_timestamp", 0)

val tvResult = findViewById<TextView>(R.id.tvResult)
tvResult.text = "Image uploaded successfully!\nURL: $uploadUrl"

// In a real app, you might want to store this URL in a database
// or share it with the user
}
}
}
}

Canceling Work

Sometimes you may need to cancel work that has been scheduled. You can cancel work by its ID, tag, or unique name.

kotlin
// Cancel by ID
WorkManager.getInstance(applicationContext).cancelWorkById(workRequest.id)

// Cancel by Tag
WorkManager.getInstance(applicationContext).cancelAllWorkByTag("sync_tag")

// Cancel unique work
WorkManager.getInstance(applicationContext).cancelUniqueWork("unique_work_name")

Summary

WorkManager is a powerful API for handling background work in Android applications. In this tutorial, we've covered:

  1. Basic setup and implementation of WorkManager
  2. Creating simple and complex workers
  3. Setting work constraints to run work under specific conditions
  4. Scheduling periodic work
  5. Chaining multiple workers for sequential execution
  6. Passing data between workers and the app
  7. A real-world example of image processing and uploading
  8. Canceling scheduled work

WorkManager handles many complexities for you, such as battery optimization, work persistence across reboots, and backwards compatibility across different Android versions.

Additional Resources

Exercises

  1. Create a WorkManager implementation that downloads multiple files in parallel and then processes them when all downloads are complete.
  2. Implement a periodic work request that syncs data with a remote server every hour, but only when on an unmetered network.
  3. Build an app that uses WorkManager to compress all images in a specific folder when the device is idle and charging.
  4. Extend the image processing example to implement progress tracking and user notifications during the process.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)