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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

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.

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.

You should see the bitmap files with the sepia filter applied:

Sepia filter files

If you don’t see them, try synchronizing:
Device file explorer

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.
Clean 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.