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 3 of 4 of this article. Click here to view the first page.

Chaining FilterWorker with CompressWorker

Open MainActivity again and modify onActivityResult() as follows:

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 zipFiles = OneTimeWorkRequest.Builder(CompressWorker::class.java).build()

    val workManager = WorkManager.getInstance()
    workManager.beginWith(applySepiaFilter)
        .then(zipFiles)
        .enqueue()
    }
}

You’ve combined the zip compression with the sepia filter code. After you created a CompressWorker zipFiles, you chained it with the sepia filter worker by calling .then(zipFiles) on the workManager instance. You can chain arbitrary amounts of workers this way, and it’s really simple to do so!

Also notice how you don’t need to pass any arguments to the CompressWorker, because of the inputData and outputData constructs. When the first worker finishes, whatever output it passed on gets propagated to the next worker in chain.

Checking the Results

Run the app again. Select one or more images and, after a few seconds, check in the app files with Device File Explorer. Now, you should also see the .zip file.
Zip file

Cleaning the Worker and Uploading the ZIP

Next up, you’ll clean the directory. Additionally, after generating the .zip file, you’ll upload it to a server.

First, create a CleanFilesWorker.kt file in the workers package:

private const val LOG_TAG = "CleanFilesWorker"

class CleanFilesWorker : Worker() {

  override fun doWork(): WorkerResult = try {
    // Sleep for debugging purposes
    Thread.sleep(3000)
    Log.d(LOG_TAG, "Cleaning files!")

    ImageUtils.cleanFiles(applicationContext)

    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 pretty straightforward; it simply calls ImageUtils to clean up the files.

To finish, create the UploadWorker.kt file inside workers with the following content:

private const val LOG_TAG = "UploadWorker"
private const val KEY_ZIP_PATH = "ZIP_PATH"

class UploadWorker : Worker() {

  override fun doWork(): WorkerResult = try {
    // Sleep for debugging purposes
    Thread.sleep(3000)
    Log.d(LOG_TAG, "Uploading file!")

    val zipPath = inputData.getString(KEY_ZIP_PATH, null)

    ImageUtils.uploadFile(Uri.parse(zipPath))

    Log.d(LOG_TAG, "Success!")
    WorkerResult.SUCCESS
  } catch (e: Throwable) {
    Log.e(LOG_TAG, "Error executing work: " + e.message, e)
    WorkerResult.FAILURE
  }
}

UploadWorker is simple, too; it parses the .zip file path and tells ImageUtils to upload the final product.

Now, create the new workers and add them to the chain them accordingly in MainActivity:

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 zipFiles = OneTimeWorkRequest.Builder(CompressWorker::class.java).build()
    val uploadZip = OneTimeWorkRequest.Builder(UploadWorker::class.java).build()
    val cleanFiles = OneTimeWorkRequest.Builder(CleanFilesWorker::class.java).build()

    val workManager = WorkManager.getInstance()
    workManager.beginWith(cleanFiles)
        .then(applySepiaFilter)
        .then(zipFiles)
        .then(uploadZip)
        .enqueue()
    }
}

Note: Since the UploadWorker needs to upload the file somewhere, you need a file server to be running. You can do this in two different ways: run one locally or upload to a shared service.

Both options are ready for you. There’s a shared service running to which you can upload at any time. If, however, you prefer to upload locally, you can use the prepared local version of a file server.

To run the local version, you need to install NodeJS, and then navigate in the terminal to the server folder, found in the materials you downloaded. Execute the command node app.js, and everything should be up and running.

Depending on the chosen server option, open ImageUtils.kt and comment out one of the two SERVER_UPLOAD_PATH constants and uncomment the other one. The one containing 10.0.2.2:3000 is used for local server, while the other one is the shared service. Feel free to change the local server IP address to something else, if you’re using a real device.

Checking the Results

Run the app, again. Next, select one or more images and, after a few seconds, open Logcat (View ▸ Tool Windows ▸ Logcat). You should see something like this, confirming that the file was correctly received by the server:
onResponse - Status: 200 Body: {"url":"/files/Your-UUID-string.zip"}

If you are running the server locally, you can open a browser and go to http://localhost:3000/files/Your-UUID-string.zip to download the file.

Starting Unique Work

Worker tasks can be started in two different ways. First, you can call the beginWith() method, as you’ve done so far. The second way is by calling beginUniqueWork() instead. As the name suggests, the beginUniqueWork() method starts work that can only have one unique instance. But you also have to provide something called an ExistingWorkPolicy. If you try to start another work instance, the previous work will proceed according to the chosen policy — replace, keep or append:

  • Replace the existing sequence with the new one.
  • Just ignore the new one and keep the existing sequence.
  • Append the new sequence to the existing one.

Replacing Existing Work of Picked Photos

For this tutorial, you’ll replace the existing work. To do that, open MainActivity, add a constant to the companion object and change onActivityResult() as follows:

class MainActivity : AppCompatActivity() {

  companion object {
    ...
    private const val UNIQUE_WORK_NAME = "UNIQUE_WORK_NAME"
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
    if (resultCode == Activity.RESULT_OK
        && requestCode == GALLERY_REQUEST_CODE) {

      val applySepiaFilter = buildSepiaFilterRequests(data)
      val zipFiles = OneTimeWorkRequest.Builder(CompressWorker::class.java).build()
      val uploadZip = OneTimeWorkRequest.Builder(UploadWorker::class.java).build()
      val cleanFiles = OneTimeWorkRequest.Builder(CleanFilesWorker::class.java).build()

      val workManager = WorkManager.getInstance()

      workManager.beginUniqueWork(UNIQUE_WORK_NAME, ExistingWorkPolicy.REPLACE, cleanFiles)
          .then(applySepiaFilter)
          .then(zipFiles)
          .then(uploadZip)
          .enqueue()
    }
  }
  ...
}

By calling beginUniqueWork(UNIQUE_WORK_NAME, ExistingWorkPolicy.REPLACE, cleanFiles), you’ve started unique work and decided to use the REPLACE policy. This means that, each time that you click the Pick Photos button and select one or more images, WorkManager will replace the existing sequence of WorkRequests and start a new one.

Observing Work

WorkManager also allows you to observe the progress of each work instance using the following:

  • getStatusById(): Use this to get a LiveData object containing the WorkStatus for the WorkRequest with a given ID.
  • getStatusesByTag(): Returns a LiveData object containing the list of WorkStatus objects for the specified tag. This is possible because you can tag multiple WorkRequests with the same name.
  • getStatusesForUniqueWork(): This will also return a LiveData object containing the list of WorkStatus objects that have the same unique work name.

Tagging a WorkRequest and Observing It

You’ll observe the work of the requests to toggle a progress bar and a cancel button. Open MainActivity and add a tag to the uploadZip work request using a new constant WORK_TAG:

class MainActivity : AppCompatActivity() {

  companion object {
    ...
    private const val WORK_TAG = "WORK_TAG"
  }  


  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent) {
      ...
      val uploadZip = OneTimeWorkRequest.Builder(UploadWorker::class.java)
          .addTag(WORK_TAG)
          .build()

After you’ve added a tag to the worker, you can observe its status. Now, add the following method to MainActivity, which will do just that:

...
import android.arch.lifecycle.Observer
...

private fun observeWork() {
  val statuses = WorkManager.getInstance().getStatusesByTag(WORK_TAG)
  statuses.observe(this,
      Observer<List<WorkStatus>> { workStatusList ->
        val currentWorkStatus = workStatusList?.getOrNull(0)
        val isWorkActive = currentWorkStatus?.state?.isFinished == false

        val uploadVisibility = if (isWorkActive) View.VISIBLE else View.GONE

        uploadGroup.visibility = uploadVisibility
      })
}

getStatusesByTag() returns a LiveData object to which you attach an Observer. When you get an update on the status from the WorkManager, you check if it’s active or not. Since you’ve tagged only one worker, you can take the first item from the list (if it exists) and check its state. If it’s not finished, you show a progress bar and a cancel button. Otherwise, you hide them.

Now, at the bottom of the onCreate() lifecycle method in MainActivity, add a call observeWork().

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

  requestPermissionsIfNecessary()
    
  observeWork()
}