Background work with WorkManager - Kotlin

1. Introduction

There are many options on Android for deferrable background work. This codelab covers WorkManager, a backwards compatible, flexible and simple library for deferrable background work. WorkManager is the recommended task scheduler on Android for deferrable work, with a guarantee to be executed.

What is WorkManager

WorkManager is part of Android Jetpack and an Architecture Component for background work that needs a combination of opportunistic and guaranteed execution. Opportunistic execution means that WorkManager will do your background work as soon as it can. Guaranteed execution means that WorkManager will take care of the logic to start your work under a variety of situations, even if you navigate away from your app.

WorkManager is an incredibly flexible library that has many additional benefits. These include:

  • Support for both asynchronous one-off and periodic tasks
  • Support for constraints such as network conditions, storage space, and charging status
  • Chaining of complex work requests, including running work in parallel
  • Output from one work request used as input for the next
  • Handling API level compatibility back to API level 14 (see note)
  • Working with or without Google Play services
  • Following system health best practices
  • LiveData support to easily display work request state in UI

When to use WorkManager

The WorkManager library is a good choice for tasks that are useful to complete, even if the user navigates away from the particular screen or your app.

Some examples of tasks that are a good use of WorkManager:

  • Uploading logs
  • Applying filters to images and saving the image
  • Periodically syncing local data with the network

WorkManager offers guaranteed execution, and not all tasks require that. As such, it is not a catch-all for running every task off of the main thread. For more details about when to use WorkManager, check out the Guide to background processing.

What you'll build

These days, smartphones are almost too good at taking pictures. Gone are the days a photographer could take a reliably blurry picture of something mysterious.

In this codelab you'll be working on Blur-O-Matic, an app that blurs photos and saves the result to a file. Was that the Loch Ness monster or evelopera toy submarine? With Blur-O-Matic, no-one will ever know.

Image of app in completed state, with a placeholder image of the cupcake, 3 options for blurriness to apply on image, and 2 buttons. One to start blurring the image, and one to see the blurred image.

Blurred image as seen after clicking 'See File'.

What you'll learn

  • Adding WorkManager to your project
  • Scheduling a simple task
  • Input and output parameters
  • Chaining work
  • Unique work
  • Displaying work status in the UI
  • Cancelling work
  • Work constraints

What you'll need

2. Getting set up

Step 1 - Download the Code

Click the following link to download all the code for this codelab:

Or if you prefer you can clone the WorkManager codelab from GitHub:

$ git clone -b start_kotlin https://github.com/googlecodelabs/android-workmanager

Step 2 - Run the app

Run the app. You should see the following screen:

9e4707e0fbdd93c7.png

The screen should have radio buttons where you can select how blurry you'd like your image to be. Pressing the Go button will eventually blur and save the image.

As of now, the app does not apply any blurring.

The starting code contains:

  • WorkerUtils: This class contains the code for actually blurring an image, and a few convenience methods which you'll use later to display Notifications, save a bitmap to file, and slow down the app.
  • BlurActivity:* The activity which shows the image and includes radio buttons for selecting blur level.
  • BlurViewModel:* This view model stores all of the data needed to display the BlurActivity. It will also be the class where you start the background work using WorkManager.
  • Constants: A static class with some constants you'll use during the codelab.
  • res/activity_blur.xml: the layout files for BlurActivity.

***** These are the only files you'll write code in.

3. Add WorkManager to your app

WorkManager requires the gradle dependency below. These have been already included in the build files:

app/build.gradle

dependencies {
    // WorkManager dependency
    implementation "androidx.work:work-runtime-ktx:$versions.work"
}

You should get the most current stable version of work-runtime-ktx from here and put the correct version in. At this moment the latest version is:

build.gradle

versions.work = "2.7.1"

If you update your version to a newer one, make sure to Sync Now to sync your project with the changed gradle files.

4. Make your first WorkRequest

In this step you will take an image in the res/drawable folder called android_cupcake.png and run a few functions on it in the background. These functions will blur the image and save it to a temporary file.

WorkManager Basics

There are a few WorkManager classes you need to know about:

  • Worker: This is where you put the code for the actual work you want to perform in the background. You'll extend this class and override the doWork() method.
  • WorkRequest: This represents a request to do some work. You'll pass in your Worker as part of creating your WorkRequest. When making the WorkRequest you can also specify things like Constraints on when the Worker should run.
  • WorkManager: This class actually schedules your WorkRequest and makes it run. It schedules WorkRequests in a way that spreads out the load on system resources, while honoring the constraints you specify.

In your case, you'll define a new BlurWorker which will contain the code to blur an image. When the Go button is clicked, a WorkRequest is created and then enqueued by WorkManager.

Step 1 - Make BlurWorker

In the package workers, create a new Kotlin class called BlurWorker.

Step 2 - Add a constructor

Add a dependency to Worker for the BlurWorker class:

class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}

Step 3 - Override and implement doWork()

Your Worker will blur the cupcake image shown.

To better see when work is being executed, you will be utilizing WorkerUtil's makeStatusNotification(). This method will let you easily display a notification banner at the top of the screen.

Override the doWork() method and then implement the following. You can refer to the completed code at the end of the section:

  1. Get a Context by calling the applicationContext property. Assign it to a new val named appContext. You'll need this for various bitmap manipulations you're about to do.
  2. Display a status notification using the function, makeStatusNotification to notify the user about blurring the image.
  3. Create a Bitmap from the cupcake image:
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.android_cupcake)
  1. Get a blurred version of the bitmap by calling the blurBitmap method from WorkerUtils.
  2. Write that bitmap to a temporary file by calling the writeBitmapToFile method from WorkerUtils. Make sure to save the returned URI to a local variable.
  3. Make a Notification displaying the URI by calling the makeStatusNotification method from WorkerUtils.
  4. Return Result.success().
  5. Wrap the code from steps 3-6 in a try/catch statement. Catch a generic Throwable.
  6. In the catch statement, print an error message using the Log statement: Log.e(TAG, "Error applying blur").
  7. In the catch statement then return Result.failure().

The completed code for this step is below.

BlurWorker.kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R

private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override fun doWork(): Result {
        val appContext = applicationContext

        makeStatusNotification("Blurring image", appContext)

        return try {
            val picture = BitmapFactory.decodeResource(
                    appContext.resources,
                    R.drawable.android_cupcake)

            val output = blurBitmap(picture, appContext)

            // Write bitmap to a temp file
            val outputUri = writeBitmapToFile(appContext, output)

            makeStatusNotification("Output is $outputUri", appContext)

            Result.success()
        } catch (throwable: Throwable) {
            Log.e(TAG, "Error applying blur")
            Result.failure()
        }
    }
}

Step 4 - Get WorkManager in the ViewModel

Create a class variable for a WorkManager instance in your ViewModel:

BlurViewModel.kt

private val workManager = WorkManager.getInstance(application)

Step 5 - Enqueue the WorkRequest in WorkManager

Alright, time to make a WorkRequest and tell WorkManager to run it. There are two types of WorkRequests:

  • OneTimeWorkRequest: A WorkRequest that will only execute once.
  • PeriodicWorkRequest: A WorkRequest that will repeat on a cycle.

We only want the image to be blurred once when the Go button is clicked. The applyBlur method is called when the Go button is clicked, so create a OneTimeWorkRequest from BlurWorker there. Then, using your WorkManager instance, enqueue your WorkRequest.

Add the following line of code into BlurViewModel's applyBlur() method:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
   workManager.enqueue(OneTimeWorkRequest.from(BlurWorker::class.java))
}

Step 6 - Run your code!

Run your code. It should compile and you should see the Notification when you press the Go button. Note that in order to see a more blurred result, you should select ‘More blurred' or ‘The most blurred' option.

e9d67a9b01039514.png

To confirm that the image was successfully blurred, you can open the Device File Explorer in Android Studio:

267de13909ae6ce9.png

Then navigate to data > data > com.example.background > files > blur_filter_outputs> <URI> and confirm that the cupcake was in fact blurred:

e1f61035d680ba03.png

5. Add input and output

Blurring the image asset in the resources directory is all well and good, but for Blur-O-Matic to really be the revolutionary image editing app it's destined to be, you should let the user blur the image they see on screen and then be able to show them the blurred result.

To do this, we'll provide the URI of the cupcake image displayed as input to our WorkRequest displayed and then use the output of our WorkRequest to display the final blurred image.

Step 1 - Create a Data input object

Input and output is passed in and out via Data objects. Data objects are lightweight containers for key/value pairs. They are meant to store a small amount of data that might pass into and out from WorkRequests.

You're going to pass in the URI for the user's image into a bundle. That URI is stored in a variable called imageUri.

In BlurViewModel, create a private method called createInputDataForUri. This method should:

  1. Create a Data.Builder object. Import androidx.work.Data, when requested.
  2. If imageUri is a non-null URI, then add it to the Data object using the putString method. This method takes a key and a value. You can use the String constant KEY_IMAGE_URI from the Constants class.
  3. Call build() on the Data.Builder object to make your Data object, and return it.

Below is the completed createInputDataForUri method:

BlurViewModel.kt

/**
 * Creates the input data bundle which includes the Uri to operate on
 * @return Data which contains the Image Uri as a String
 */
private fun createInputDataForUri(): Data {
    val builder = Data.Builder()
    imageUri?.let {
        builder.putString(KEY_IMAGE_URI, imageUri.toString())
    }
    return builder.build()
}

Step 2 - Pass the Data object to WorkRequest

You're going to change the applyBlur method in BlurViewModel so that it:

  1. Creates a new OneTimeWorkRequestBuilder.
  2. Calls setInputData, passing in the result from createInputDataForUri.
  3. Builds the OneTimeWorkRequest.
  4. Enqueues the work request using WorkManager request so that the work will be scheduled to run.

Below is the completed applyBlur method:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
            .setInputData(createInputDataForUri())
            .build()

    workManager.enqueue(blurRequest)
}

Step 3 - Update BlurWorker's doWork() to get the input

Now let's update BlurWorker's doWork() method to get the URI we passed in from the Data object:

BlurWorker.kt

override fun doWork(): Result {
    val appContext = applicationContext

    // ADD THIS LINE
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    // ... rest of doWork()
}

Step 4 - Blur the given URI

With the URI, now let's blur the image of the cupcake on screen.

  1. Remove the previous code that was getting the image resource.

val picture = BitmapFactory.decodeResource(appContext.resources, R.drawable.android_cupcake)

  1. Check that resourceUri obtained from the Data that was passed in is not empty.
  2. Assign the picture variable to be the image that was passed in like so:

val picture = BitmapFactory.decodeStream(

appContext.contentResolver.

  `openInputStream(Uri.parse(resourceUri)))`

BlurWorker.kt

override fun doWork(): Result {
    val appContext = applicationContext

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    return try {
        // REMOVE THIS
        //    val picture = BitmapFactory.decodeResource(
        //            appContext.resources,
        //            R.drawable.android_cupcake)

        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

        // Write bitmap to a temp file
        val outputUri = writeBitmapToFile(appContext, output)

        Result.success()
    } catch (throwable: Throwable) {
        Log.e(TAG, "Error applying blur")
        throwable.printStackTrace()
        Result.failure()
    }
}

Step 5 - Output temporary URI

You're now done with this Worker and can return the output URI in Result.success(). Provide the Output URI as an output Data to make this temporary image easily accessible to other workers for further operations. This will be useful in the next chapter when you create a Chain of workers. To do this:

  1. Create a new Data, just as you did with the input, and store outputUri as a String. Use the same key, KEY_IMAGE_URI.
  2. Return this to WorkManager using Result.success(Data outputData) method.

BlurWorker.kt

Modify the Result.success() line in doWork() to:

val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

Result.success(outputData)

Step 6 - Run your app

At this point you should run your app. It should compile and have the same behavior where you can see the blurred image through Device File Explorer, but not on screen yet.

To check for another blurred image, you can open the Device File Explorer in Android Studio and navigate to data/data/com.example.background/files/blur_filter_outputs/<URI> as you did in the last step.

Note that you might need to Synchronize to see your images:

3e845e1040e0087b.png

Great work! You've blurred an input image using WorkManager!

6. Chain your Work

Right now you're doing a single work task: blurring the image. This is a great first step, but is missing some core functionality:

  • It doesn't clean up temporary files.
  • It doesn't actually save the image to a permanent file.
  • It always blurs the picture the same amount.

We'll use a WorkManager chain of work to add this functionality.

WorkManager allows you to create separate WorkerRequests that run in order or parallel. In this step you'll create a chain of work that looks like this:

54832b34e9c9884a.png

The WorkRequests are represented as boxes.

Another really neat feature of chaining is that the output of one WorkRequest becomes the input of the next WorkRequest in the chain. The input and output that is passed between each WorkRequest is shown as blue text.

Step 1 - Create Cleanup and Save Workers

First, you'll define all the Worker classes you need. You already have a Worker for blurring an image, but you also need a Worker which cleans up temp files and a Worker which saves the image permanently.

Create two new classes in the workers package which extend Worker.

The first should be called CleanupWorker, the second should be called SaveImageToFileWorker.

Step 2 - Make it extend Worker

Extend CleanupWorker class from Worker class. Add the required constructor parameters.

class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}

Step 3 - Override and implement doWork() for CleanupWorker

CleanupWorker doesn't need to take any input or pass any output. It always deletes the temporary files if they exist. Since file manipulation is out of scope for this codelab, you can copy the code for the CleanupWorker below:

CleanupWorker.kt

package com.example.background.workers

import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override fun doWork(): Result {
        // Makes a notification when the work starts and slows down the work so that
        // it's easier to see each WorkRequest start, even on emulated devices
        makeStatusNotification("Cleaning up old temporary files", applicationContext)
        sleep()

        return try {
            val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
            if (outputDirectory.exists()) {
                val entries = outputDirectory.listFiles()
                if (entries != null) {
                    for (entry in entries) {
                        val name = entry.name
                        if (name.isNotEmpty() && name.endsWith(".png")) {
                            val deleted = entry.delete()
                            Log.i(TAG, "Deleted $name - $deleted")
                        }
                    }
                }
            }
            Result.success()
        } catch (exception: Exception) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

Step 4 - Override and implement doWork() for SaveImageToFileWorker

SaveImageToFileWorker will take input and output. The input is a String of the temporarily blurred image URI stored with the key KEY_IMAGE_URI. And the output will also be a String, the URI of the saved blurred image stored with the key KEY_IMAGE_URI.

4fc29ac70fbecf85.png

Since this is still not a codelab about file manipulations, the code is provided below. Notice how the resourceUri and output values are retrieved with the key KEY_IMAGE_URI. This is very similar to the code you wrote in the last step for input and output (it uses all the same keys).

SaveImageToFileWorker.kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    private val title = "Blurred Image"
    private val dateFormatter = SimpleDateFormat(
            "yyyy.MM.dd 'at' HH:mm:ss z",
            Locale.getDefault()
    )

    override fun doWork(): Result {
        // Makes a notification when the work starts and slows down the work so that
        // it's easier to see each WorkRequest start, even on emulated devices
        makeStatusNotification("Saving image", applicationContext)
        sleep()

        val resolver = applicationContext.contentResolver
        return try {
            val resourceUri = inputData.getString(KEY_IMAGE_URI)
            val bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri)))
            val imageUrl = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, title, dateFormatter.format(Date()))
            if (!imageUrl.isNullOrEmpty()) {
                val output = workDataOf(KEY_IMAGE_URI to imageUrl)

                Result.success(output)
            } else {
                Log.e(TAG, "Writing to MediaStore failed")
                Result.failure()
            }
        } catch (exception: Exception) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

Step 5 - Modify BlurWorker Notification

Now that we have a chain of Workers taking care of saving the image in the correct folder, we can slow down the work by using the sleep() method defined in the WorkerUtils class, so that it's easier to see each WorkRequest start, even on emulated devices. The final version of BlurWorker become:

BlurWorker.kt

class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

override fun doWork(): Result {
    val appContext = applicationContext

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    // ADD THIS TO SLOW DOWN THE WORKER
    sleep() 
    // ^^^^
    
    return try {
        if (TextUtils.isEmpty(resourceUri)) {
            Timber.e("Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

        // Write bitmap to a temp file
        val outputUri = writeBitmapToFile(appContext, output)

        val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

        Result.success(outputData)
    } catch (throwable: Throwable) {
        throwable.printStackTrace()
        Result.failure()
    }
}

Step 6 - Create a WorkRequest chain

You need to modify the BlurViewModel's applyBlur method to execute a chain of WorkRequests instead of just one. Currently the code looks like this:

BlurViewModel.kt

val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
        .setInputData(createInputDataForUri())
        .build()

workManager.enqueue(blurRequest)

Instead of calling workManager.enqueue(), call workManager.beginWith(). This returns a WorkContinuation, which defines a chain of WorkRequests. You can add to this chain of work requests by calling then() method, for example, if you have three WorkRequest objects, workA, workB, and workC, you could do the following:

// Example code, don't copy to the project
val continuation = workManager.beginWith(workA)

continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
        .then(workC)
        .enqueue() // Enqueues the WorkContinuation which is a chain of work 

This would produce and run the following chain of WorkRequests:

bf3b82eb9fd22349.png

Create a chain of a CleanupWorker WorkRequest, a BlurImage WorkRequest and a SaveImageToFile WorkRequest in applyBlur. Pass input into the BlurImage WorkRequest.

The code for this is below:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    // Add WorkRequest to Cleanup temporary images
    var continuation = workManager
            .beginWith(OneTimeWorkRequest
            .from(CleanupWorker::class.java))

    // Add WorkRequest to blur the image
    val blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)
            .setInputData(createInputDataForUri())
            .build()

    continuation = continuation.then(blurRequest)
    
    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequest.Builder(SaveImageToFileWorker::class.java).build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

This should compile and run. You should now be able to hit the Go button and see notifications when the different workers are executing. You will still be able to see the blurred image in Device File Explorer, and in an upcoming step you'll be adding an extra button so that users can see the blurred image on the device.

In the screenshots below, you'll notice that the notification messages displays which worker is currently running.

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

Step 7 - Repeat the BlurWorker

Time to add the ability to blur the image by different amounts. Take the blurLevel parameter passed into applyBlur and add that many blur WorkRequest operations to the chain. Only the first WorkRequest needs and should take in the uri input.

Try it yourself and then compare with the code below:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    // Add WorkRequest to Cleanup temporary images
    var continuation = workManager
            .beginWith(OneTimeWorkRequest
            .from(CleanupWorker::class.java))

    // Add WorkRequests to blur the image the number of times requested
    for (i in 0 until blurLevel) {
        val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

        // Input the Uri if this is the first blur operation
        // After the first blur operation the input will be the output of previous
        // blur operations.
        if (i == 0) {
            blurBuilder.setInputData(createInputDataForUri())
        }

        continuation = continuation.then(blurBuilder.build())
    }

    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
            .build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

Open the device file explorer, to see the blurred images. Notice that the output folder contains multiple blurred images, images that are in the intermediate stages of being blurred, and the final image that displays the blurred image based on the blur amount you selected.

Superb "work"! Now you can blur an image as much or as little as you want! How mysterious!

7. Ensure unique work

Now that you've used chains, it's time to tackle another powerful feature of WorkManager - unique work chains.

Sometimes you only want one chain of work to run at a time. For example, perhaps you have a work chain that syncs your local data with the server - you probably want to let the first data sync finish before starting a new one. To do this, you would use beginUniqueWork instead of beginWith; and you provide a unique String name. This names the entire chain of work requests so that you can refer to and query them together.

Ensure that your chain of work to blur your file is unique by using beginUniqueWork. Pass in IMAGE_MANIPULATION_WORK_NAME as the key. You'll also need to pass in a ExistingWorkPolicy. Your options are REPLACE, KEEP or APPEND.

You'll use REPLACE because if the user decides to blur another image before the current one is finished, we want to stop the current one and start blurring the new image.

The code for starting your unique work continuation is below:

BlurViewModel.kt

// REPLACE THIS CODE:
// var continuation = workManager
//            .beginWith(OneTimeWorkRequest
//            .from(CleanupWorker::class.java))
// WITH
var continuation = workManager
        .beginUniqueWork(
                IMAGE_MANIPULATION_WORK_NAME,
                ExistingWorkPolicy.REPLACE,
                OneTimeWorkRequest.from(CleanupWorker::class.java)
        )

Blur-O-Matic will now only ever blur one picture at a time.

8. Tag and display Work status

This section uses LiveData heavily, so to fully grasp what's going on you should be familiar with LiveData. LiveData is an observable, lifecycle-aware data holder.

You can check out the documentation or the Android Lifecycle-aware components Codelab if this is your first time working with LiveData or observables.

The next big change you'll do is to actually change what's showing in the app as the Work executes.

You can get the status of any WorkRequest by getting a LiveData that holds a WorkInfo object. WorkInfo is an object that contains details about the current state of a WorkRequest, including:

The following table shows three different ways to get LiveData<WorkInfo> or LiveData<List<WorkInfo>> objects and what each does.

Type

WorkManager Method

Description

Get work using id

getWorkInfoByIdLiveData

Each WorkRequest has a unique ID generated by WorkManager; you can use this to get a single LiveData<WorkInfo> for that exact WorkRequest.

Get work using unique chain name

getWorkInfosForUniqueWorkLiveData

As you've just seen, WorkRequests can be part of a unique chain. This returns LiveData<List<WorkInfo>> for all work in a single, unique chain of WorkRequests.

Get work using a tag

getWorkInfosByTagLiveData

Finally, you can optionally tag any WorkRequest with a String. You can tag multiple WorkRequests with the same tag to associate them. This returns the LiveData<List<WorkInfos>> for any single tag.

You'll be tagging the SaveImageToFileWorker WorkRequest, so that you can get it using getWorkInfosByTag. You'll use a tag to label your work instead of using the WorkManager ID, because if your user blurs multiple images, all of the saving image WorkRequests will have the same tag but not the same ID. Also you are able to pick the tag.

You would not use getWorkInfosForUniqueWork because that would return the WorkInfo for all of the blur WorkRequests and the cleanup WorkRequest as well; it would take extra logic to find the save image WorkRequest.

Step 1 - Tag your work

In applyBlur, when creating the SaveImageToFileWorker, tag your work using the String constant TAG_OUTPUT :

BlurViewModel.kt

val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .addTag(TAG_OUTPUT) // <-- ADD THIS
        .build()

Step 2 - Get the WorkInfo

Now that you've tagged the work, you can get the WorkInfo:

  1. In BlurViewModel, declare a new class variable called outputWorkInfos which is a LiveData<List<WorkInfo>>
  2. In BlurViewModel add an init block to get the WorkInfo using WorkManager.getWorkInfosByTagLiveData

The code you need is below:

BlurViewModel.kt

// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>

// Modify the existing init block in the BlurViewModel class to this:
init {
    imageUri = getImageUri(application.applicationContext)
    // This transformation makes sure that whenever the current work Id changes the WorkInfo
    // the UI is listening to changes
    outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
} 

Step 3 - Display the WorkInfo

Now that you have a LiveData for your WorkInfo, you can observe it in the BlurActivity. In the observer:

  1. Check if the list of WorkInfo is not null and if it has any WorkInfo objects in it - if not then the Go button has not been clicked yet, so return.
  2. Get the first WorkInfo in the list; there will only ever be one WorkInfo tagged with TAG_OUTPUT because we made the chain of work unique.
  3. Check whether the work status is finished, using workInfo.state.isFinished.
  4. If it's not finished, then call showWorkInProgress() which hides the Go button and shows the Cancel Work button and progress bar.
  5. If it's finished then call showWorkFinished()which hides the Cancel Work button and progress bar and displays the Go button.

Here's the code:

Note: Import androidx.lifecycle.Observer when requested.

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Observe work status, added in onCreate()
    viewModel.outputWorkInfos.observe(this, workInfosObserver())
}

// Define the observer function
private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()
        } else {
            showWorkInProgress()
        }
    }
}

Step 4 - Run your app

Run your app - it should compile and run, and now show a progress bar when it's working, as well as the cancel button:

7b70288f69050f0b.png

9. Show final output

Each WorkInfo also has a getOutputData method which allows you to get the output Data object with the final saved image. In Kotlin you can access this method using a variable that the language generates for you: outputData. Let's display a button that says See File whenever there's a blurred image ready to show.

Step 1 - Create the ‘See File' button

There's already a button in the activity_blur.xml layout that is hidden. It's in BlurActivity and called outputButton.

In BlurActivity, inside onCreate(), set up the click listener for that button. It should get the URI and then open up an activity to view that URI. You can use the code below:

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   // Setup view output image file button
   binding.seeFileButton.setOnClickListener {
       viewModel.outputUri?.let { currentUri ->
           val actionView = Intent(Intent.ACTION_VIEW, currentUri)
           actionView.resolveActivity(packageManager)?.run {
               startActivity(actionView)
           }
       }
   }
}

Step 2 - Set the URI and show the button

There are a few final tweaks you need to apply to the WorkInfo observer to get this to work (no pun intended):

  1. If the WorkInfo is finished, get the output data, using workInfo.outputData.
  2. Then get the output URI, remember that it's stored with the Constants.KEY_IMAGE_URI key.
  3. Then if the URI isn't empty, it is saved properly; show the outputButton and call setOutputUri on the view model with the uri.

BlurActivity.kt

private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()

            // Normally this processing, which is not directly related to drawing views on
            // screen would be in the ViewModel. For simplicity we are keeping it here.
            val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)

            // If there is an output file show "See File" button
            if (!outputImageUri.isNullOrEmpty()) {
                viewModel.setOutputUri(outputImageUri)
                binding.seeFileButton.visibility = View.VISIBLE
            }
        } else {
            showWorkInProgress()
        }
    }
}

Step 3 - Run your code

Run your code. You should see your new, clickable See File button which takes you to the outputted file:

5366222d0b4fb705.png

cd1ecc8b4ca86748.png

10. Cancel work

632d75e145022d14.png

You added this Cancel Work button, so let's add the code to make it do something. With WorkManager, you can cancel work using the id, by tag and by unique chain name.

In this case, you'll want to cancel work by unique chain name, because you want to cancel all work in the chain, not just a particular step.

Step 1 - Cancel the work by name

In BlurViewModel, add a new method called cancelWork() to cancel the unique work. Inside the function call cancelUniqueWork on the workManager, pass in the tag IMAGE_MANIPULATION_WORK_NAME.

BlurViewModel.kt

internal fun cancelWork() {
    workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}

Step 2 - Call cancel method

Then, hook up the button cancelButton to call cancelWork:

BlurActivity.kt

// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }

Step 3 - Run and cancel your work

Run your app. It should compile just fine. Start blurring a picture and then click the cancel button. The whole chain is cancelled!

cf55bb104ed09d95.png

Note now there is only the GO button once work is cancelled because WorkState is no longer in a FINISHED state.

11. Work constraints

Last but not least, WorkManager supports Constraints. For Blur-O-Matic, you'll use the constraint that the device must be charging. This means that your work request will only run if the device is charging.

Step 1 - Create and add charging constraint

To create a Constraints object, you use a Constraints.Builder. Then you set the constraints you want and add it to the WorkRequest using the method, setRequiresCharging() as shown below:

Import androidx.work.Constraints when requested.

BlurViewModel.kt

// Put this inside the applyBlur() function, above the save work request.
// Create charging constraint
val constraints = Constraints.Builder()
        .setRequiresCharging(true)
        .build()

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .setConstraints(constraints)
        .addTag(TAG_OUTPUT)
        .build()
continuation = continuation.then(save)

// Actually start the work
continuation.enqueue()

Step 2 - Test with emulator or device

Now you can run Blur-O-Matic. If you're on a device, you can remove or plug in your device. On an emulator, you can change the charging status in the Extended controls window:

406ce044ca07169f.png

When the device is not charging, it should suspend SaveImageToFileWorker, executing it only after that you plug it in.

302da5ec986ae769.png

12. Congratulations

Congratulations! You've finished the Blur-O-Matic app and in the process learned about:

  • Adding WorkManager to your Project
  • Scheduling a OneTimeWorkRequest
  • Input and Output parameters
  • Chaining work together WorkRequests
  • Naming Unique WorkRequest chains
  • Tagging WorkRequests
  • Displaying WorkInfo in the UI
  • Cancelling WorkRequests
  • Adding constraints to a WorkRequest

Excellent "work"! To see the end state of the code and all the changes check out:

Or if you prefer you can clone the WorkManager's codelab from GitHub:

$ git clone https://github.com/googlecodelabs/android-workmanager

WorkManager supports a lot more than we could cover in this codelab, including repetitive work, a testing support library, parallel work requests and input mergers. To learn more, head over to the WorkManager documentation or continue to the Advanced WorkManager codelab.