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:
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.
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.
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.
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.
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.
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:
- First, the CompressionWorkerwill run
- When it completes successfully, the UploadWorkerwill run
- When that completes successfully, the ProcessingWorkerwill run
You can also run multiple workers in parallel and then continue with another worker when they all complete:
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
// 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
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
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:
- BlurImageWorker: Takes an image URI, applies a blur filter, and saves the result
- UploadImageWorker: Takes a processed image and uploads it to a server
BlurImageWorker Implementation
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
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
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.
// 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:
- Basic setup and implementation of WorkManager
- Creating simple and complex workers
- Setting work constraints to run work under specific conditions
- Scheduling periodic work
- Chaining multiple workers for sequential execution
- Passing data between workers and the app
- A real-world example of image processing and uploading
- 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
- Create a WorkManager implementation that downloads multiple files in parallel and then processes them when all downloads are complete.
- Implement a periodic work request that syncs data with a remote server every hour, but only when on an unmetered network.
- Build an app that uses WorkManager to compress all images in a specific folder when the device is idle and charging.
- Extend the image processing example to implement progress tracking and user notifications during the process.
💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!