WorkManager Tutorial for Android: Getting Started
In this WorkManager tutorial for Android, you’ll learn how to create background tasks, how to chain tasks, and how to add constraints to each task. By Fernando Sproviero.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
WorkManager Tutorial for Android: Getting Started
30 mins
- Understanding Background Work
- Types of Background Work
- Why WorkManager?
- Knowing When to Use WorkManager
- Getting Started
- Setting Up the WorkManager Library
- Understanding WorkManager Classes
- Creating Your First Background Task
- Writing the FilterWorker Code
- Instantiating and Configuring the Worker
- Checking the Results
- Chaining Tasks
- Writing the CompressWorker Code
- Chaining FilterWorker with CompressWorker
- Checking the Results
- Cleaning the Worker and Uploading the ZIP
- Checking the Results
- Starting Unique Work
- Replacing Existing Work of Picked Photos
- Observing Work
- Tagging a WorkRequest and Observing It
- Checking the Results
- Canceling Work
- Canceling the Picking Photos Work Sequence
- Understanding Constraints
- Adding a Network Connection Constraint
- Testing the Network Constraint
- Where to Go From Here?
Writing the FilterWorker Code
To start, create a new package called workers; this will hold the code required to finish your project. Next, add a FilterWorker.kt file to it with the following content:
private const val LOG_TAG = "FilterWorker"
const val KEY_IMAGE_URI = "IMAGE_URI"
const val KEY_IMAGE_INDEX = "IMAGE_INDEX"
private const val IMAGE_PATH_PREFIX = "IMAGE_PATH_"
class FilterWorker : Worker() {
override fun doWork(): WorkerResult = try {
// Sleep for debugging purposes
Thread.sleep(3000)
Log.d(LOG_TAG, "Applying filter to image!")
val imageUriString = inputData.getString(KEY_IMAGE_URI, null)
val imageIndex = inputData.getInt(KEY_IMAGE_INDEX, 0)
val bitmap = MediaStore.Images.Media.getBitmap(applicationContext.contentResolver, Uri.parse(imageUriString))
val filteredBitmap = ImageUtils.applySepiaFilter(bitmap)
val filteredImageUri = ImageUtils.writeBitmapToFile(applicationContext, filteredBitmap)
outputData =
Data.Builder()
.putString(IMAGE_PATH_PREFIX + imageIndex, filteredImageUri.toString())
.build()
Log.d(LOG_TAG, "Success!")
WorkerResult.SUCCESS
} catch (e: Throwable) {
Log.e(LOG_TAG, "Error executing work: " + e.message, e)
WorkerResult.FAILURE
}
}
Each worker has to extend the Worker class and override the doWork
method, which returns a WorkResult. The result can succeed or fail, giving you feedback on the final outcome. Since the work being done can end with an exception, you’re wrapping the calls in a try-catch expression, and using the Kotlin single-line function syntax to return a value as the last line in each block of the try-catch.
This worker is made up of several steps. Going over each:
First, you get the image-related data from the inputData
field bound within the worker, ultimately turning it into a bitmap:
val imageUriString = inputData.getString(KEY_IMAGE_URI, null)
val imageIndex = inputData.getInt(KEY_IMAGE_INDEX, 0)
val bitmap = MediaStore.Images.Media.getBitmap(applicationContext.contentResolver, Uri.parse(imageUriString))
Second, you apply a sepia filter, using the ImageUtils
object. Right after that, you save the file to the disk:
val filteredBitmap = ImageUtils.applySepiaFilter(bitmap)
val filteredImageUri = ImageUtils.writeBitmapToFile(applicationContext, filteredBitmap)
Finally, before returning a successful result, you take the filtered image path and set it in the outputData
, which will get passed to the next worker:
outputData =
Data.Builder()
.putString("IMAGE_PATH_$imageIndex", filteredImageUri.toString())
.build()
You’ll see how this URI will be used in the next worker. Also, by returning a successful case, you notify that this worker has done its job without any issues. If there’s an exception, you return a failure to stop the work.
Note: inputData and outputData are just key-value maps. However, there is a 10KB limit for the payload.
Note: inputData and outputData are just key-value maps. However, there is a 10KB limit for the payload.
Instantiating and Configuring the Worker
Now, open the MainActivity.kt file and replace the onActivityResult
method with the following:
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
if (data != null
&& resultCode == Activity.RESULT_OK
&& requestCode == GALLERY_REQUEST_CODE) {
val applySepiaFilter = buildSepiaFilterRequests(data)
val workManager = WorkManager.getInstance()
workManager.beginWith(applySepiaFilter).enqueue()
}
}
After the images are selected and returned, you wrap each into your FilterWorker using the buildSepiaFilterRequests()
method that you’ll add next. Then you retrieve the WorkManager instance, and you begin your chain of tasks by applying sepia filters.
The code required to build the requests is as follows:
private fun buildSepiaFilterRequests(intent: Intent): List<OneTimeWorkRequest> {
val filterRequests = mutableListOf<OneTimeWorkRequest>()
intent.clipData?.run {
for (i in 0 until itemCount) {
val imageUri = getItemAt(i).uri
val filterRequest = OneTimeWorkRequest.Builder(FilterWorker::class.java)
.setInputData(buildInputDataForFilter(imageUri, i))
.build()
filterRequests.add(filterRequest)
}
}
intent.data?.run {
val filterWorkRequest = OneTimeWorkRequest.Builder(FilterWorker::class.java)
.setInputData(buildInputDataForFilter(this, 0))
.build()
filterRequests.add(filterWorkRequest)
}
return filterRequests
}
Since there are two ways to select images — by selecting a single one, and by choosing multiple — there’s also two ways to build requests. On the one hand, if there are multiple images selected, you have to run a for
loop and map each image to a filter request:
intent.clipData?.run {
for (i in 0 until itemCount) {
val imageUri = getItemAt(i).uri
val filterRequest = OneTimeWorkRequest.Builder(FilterWorker::class.java)
.setInputData(buildInputDataForFilter(imageUri, i))
.build()
filterRequests.add(filterRequest)
}
}
On the other hand, if there is only one image, you just wrap it up in a filter request:
intent.data?.run {
val filterWorkRequest = OneTimeWorkRequest.Builder(FilterWorker::class.java)
.setInputData(buildInputDataForFilter(this, 0))
.build()
filterRequests.add(filterWorkRequest)
}
In the end, you return all the requests you’ve prepared and run them all at once. Notice how they are each a OneTimeWorkRequest, meaning that this work will run once and clean up.
Add one more private method to MainActivity that creates the inputData
that each worker consumes:
private fun buildInputDataForFilter(imageUri: Uri?, index: Int): Data {
val builder = Data.Builder()
if (imageUri != null) {
builder.putString(KEY_IMAGE_URI, imageUri.toString())
builder.putInt(KEY_IMAGE_INDEX, index)
}
return builder.build()
}
Having finished all of that, you’re ready to try out the filters and see what you get!
Checking the Results
Run the app, pick one or more photos and then, after a few seconds, open the Device File Explorer via the following Android Studio menu: View ▸ Tool Windows ▸ Device File Explorer. Navigate to the /data/user/0/com.raywenderlich.android.photouploader/files/outputs
folder.
Note: On different emulators, there are different output folders. So if you cannot find the /user/0
folder, try looking up /data/data
.
Note: On different emulators, there are different output folders. So if you cannot find the /user/0
folder, try looking up /data/data
.
You should see the bitmap files with the sepia filter applied:
If you don’t see them, try synchronizing:
Congratulations! Your first worker worked just fine! :]
If you want, you can delete all the files with the Device File Explorer but, later on, you’ll create a worker that cleans these files.
Chaining Tasks
After applying the sepia filter to each selected image, you’ll compress them into a single .zip file.
Writing the CompressWorker Code
Under the workers package, create a new file called CompressWorker.kt with this content:
private const val LOG_TAG = "CompressWorker"
private const val KEY_IMAGE_PATH = "IMAGE_PATH"
private const val KEY_ZIP_PATH = "ZIP_PATH"
class CompressWorker : Worker() {
override fun doWork(): WorkerResult = try {
// Sleep for debugging purposes
Thread.sleep(3000)
Log.d(LOG_TAG, "Compressing files!")
val imagePaths = inputData.keyValueMap
.filter { it.key.startsWith(KEY_IMAGE_PATH) }
.map { it.value as String }
val zipFile = ImageUtils.createZipFile(applicationContext, imagePaths.toTypedArray())
outputData = Data.Builder()
.putString(KEY_ZIP_PATH, zipFile.path)
.build()
Log.d(LOG_TAG, "Success!")
WorkerResult.SUCCESS
} catch (e: Throwable) {
Log.e(LOG_TAG, "Error executing work: " + e.message, e)
WorkerResult.FAILURE
}
}
This worker is simpler than the last one and only consists of two steps — finding the images and zipping them.
The following snippet filters out the data, which starts with your image path format, and maps them into actual image paths:
val imagePaths = inputData.keyValueMap
.filter { it.key.startsWith(KEY_IMAGE_PATH) }
.map { it.value as String }
After mapping them, you once again call ImageUtils; this time, however, you zip the selected files. Finally, you pass the .zip file path to the next worker:
val zipFile = ImageUtils.createZipFile(applicationContext, imagePaths.toTypedArray())
outputData = Data.Builder()
.putString(KEY_ZIP_PATH, zipFile.path)
.build()
The .zip file path will then be passed along to another worker. But, for now, you’ll connect the two existing workers to create a chain that applies a filter and then zips the images.