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
CompressionWorker
will run - When it completes successfully, the
UploadWorker
will run - 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:
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 resultUploadImageWorker
: 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.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)