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
- Handles API level compatibility back to API level 14 (see note)
- Works with or without Google Play services
- Follows 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 will 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 images 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.
Hybrid striped bass photo by Peggy Greb, USDA Agricultural Research Service. |
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
- The latest Android Studio stable version
- You should also be familiar with
LiveData
andViewModel
. If you're new to these classes, check out the Android Lifecycle-aware components Codelab (specifically for ViewModel and LiveData) or Room with a View Codelab (an introduction to Architecture Components).
If you get stuck at any point...
If you get stuck with this codelab at any point or, if you want to look at the final state of the code, you can use the following link:
Or, if you prefer, you can clone the completed WorkManager's codelab from GitHub:
$ git clone https://github.com/googlecodelabs/android-workmanager
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 navigation codelab from GitHub:
$ git clone -b start_kotlin https://github.com/googlecodelabs/android-workmanager
Step 2 - Get an Image
If you're using a device where you've already downloaded or taken pictures on the device, you're all set.
If you're using a brand new device (like a recently created emulator), you'll want to either take a picture or download an image from the web using your device. Pick something mysterious!
Step 3 - Run the app
Run the app. You should see the following screens (make sure you allow the permissions to access photos from the initial prompt and if the image is disabled, reopen the app):
You can select an image and get to the next screen, which has 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:
BlurApplication
: This class contains the Application setup.WorkerUtils
: This class contains the code for actually blurring, and a few convenience methods which you'll use later to displayNotifications
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 theBlurActivity
. 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.SelectImageActivity
: The first activity which allows you to select an image.res/activity_blur.xml
andres/activity_select.xml
: The layout files for each activity.
* These are the only files you'll write code in.
WorkManager
requires the gradle dependency below. These have been already included in the build files:
app/build.gradle
dependencies {
// Other dependencies
implementation "androidx.work:work-runtime-ktx:$versions.work"
}
You should get the most current 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.3.4"
If you update your version to a newer one, make sure to Sync Now to sync your project with the changed gradle files.
In this step you will take an image in the res/drawable
folder called test.jpg
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 thedoWork()
method.WorkRequest
: This represents a request to do some work. You'll pass in yourWorker
as part of creating yourWorkRequest
. When making theWorkRequest
you can also specify things likeConstraints
on when theWorker
should run.WorkManager
: This class actually schedules yourWorkRequest
and makes it run. It schedulesWorkRequest
s 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 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 res/test.jpg
image.
Override the doWork()
method and then implement the following:
- Get a
Context
by calling theapplicationContext
property. You'll need this for various bitmap manipulations you're about to do. - Create a
Bitmap
from the test image:
val picture = BitmapFactory.decodeResource(
appContext.resources,
R.drawable.test)
- Get a blurred version of the bitmap by calling the static
blurBitmap
method fromWorkerUtils
. - Write that bitmap to a temporary file by calling the static
writeBitmapToFile
method fromWorkerUtils
. Make sure to save the returned URI to a local variable. - Make a Notification displaying the URI by calling the static
makeStatusNotification
method fromWorkerUtils
. - Return
Result.success()
- Wrap the code from steps 2-6 in a try/catch statement. Catch a generic
Throwable
. - In the catch statement, emit an error Log statement:
Timber.e(throwable, "Error applying blur")
- 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 androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R
import timber.log.Timber
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.test)
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) {
Timber.e(throwable, "Error applying blur")
Result.failure()
}
}
}
Step 4 - Get WorkManager in the ViewModel
Create a 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 WorkRequest
s:
OneTimeWorkRequest:
AWorkRequest
that will only execute once.PeriodicWorkRequest:
AWorkRequest
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.
Optionally you can open the Device File Explorer in Android Studio:
Then navigate to data>data>com.example.background>files>blur_filter_outputs><URI> and confirm that the fish was in fact blurred:
Blurring that test image is all well and good, but for Blur-O-Matic to really be the revolutionary image editing app it's destined to be, you'll need to let users blur their own images.
To do this, we'll provide the URI of the user's selected image as input to our WorkRequest
.
Step 1 - Create 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 WorkRequest
s.
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
.
Create a private method called createInputDataForUri
. This method should:
- Create a
Data.Builder
object. - If
imageUri
is a non-nullURI
, then add it to theData
object using theputString
method. This method takes a key and a value. You can use the String constantKEY_IMAGE_URI
from theConstants
class. - Call
build()
on theData.Builder
object to make yourData
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 want to change the applyBlur
method so that it:
- Creates a new
OneTimeWorkRequest.Builder
. - Calls
setInputData
, passing in the result fromcreateInputDataForUri
. - Builds the
OneTimeWorkRequest
. - Enqueues that request using
WorkManager
.
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, you can blur the image the user selected:
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.test)
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)
Result.success()
} catch (throwable: Throwable) {
Timber.e(throwable)
Result.failure()
}
}
Step 5 - Output temporary URI
We're done with this Worker and we can now return Result.success()
. We're going to provide the OutputURI 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 we're going to create a Chain of workers. To do this:
- Create a new
Data
, just as you did with the input, and storeoutputUri
as aString
. Use the same key,KEY_IMAGE_URI
. - 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.
Optionally, 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:
Great work! You've blurred an input image using WorkManager
!
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 WorkerRequest
s that run in order or parallel. In this step you'll create a chain of work that looks like this:
The WorkRequest
s 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 worker
package which extend Worker
.
The first should be called CleanupWorker
, the second should be called SaveImageToFileWorker
.
Step 2 - Make it extend Worker
Add a dependency to Worker
for the CleanupWorker
class:
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. Because this is not a codelab about file manipulation, you can copy the code for the CleanupWorker
below:
CleanupWorker.kt
package com.example.background.workers
import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
import timber.log.Timber
/**
* Cleans up temporary files generated during blurring process
*/
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()
Timber.i("Deleted $name - $deleted")
}
}
}
}
Result.success()
} catch (exception: Exception) {
Timber.e(exception)
Result.failure()
}
}
}
Step 4 - Override and implement doWork() for SaveImageToFileWorker
SaveImageToFileWorker
will take input and output. The input is a String
stored with the key KEY_IMAGE_URI
. And the output will also be a String
stored with the key KEY_IMAGE_URI
.
Since this is still not a codelab about file manipulations, the code is below, with two TODO
s for you to fill in the appropriate code for input and output. 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 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
import timber.log.Timber
/**
* Saves the image to a permanent file
*/
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 {
Timber.e("Writing to MediaStore failed")
Result.failure()
}
} catch (exception: Exception) {
Timber.e(exception)
Result.failure()
}
}
}
Step 5 - Modify BlurWorker Notification
Now that we have a chain of Worker
s taking care of saving the image in the correct folder, we can slow down the work 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) {
Timber.e(throwable)
Result.failure()
}
}
Step 6 - Create a WorkRequest Chain
You need to modify the BlurViewModel
's applyBlur
method to execute a chain of WorkRequest
s 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 WorkRequest
s. 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:
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 be able to see whatever image you choose to blur now saved in your Pictures folder:
Step 7 - Repeat the BlurWorker
Time to add the ability to blur the image 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()
}
Superb "work"! Now you can blur an image as much or as little as you want! How mysterious!
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.
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:
- Whether the work is
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
orSUCCEEDED
- If the
WorkRequest
is finished, any output data from the work.
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 |
| Each |
Get work using unique chain name |
| As you've just seen, |
Get work using a tag |
| Finally, you can optionally tag any WorkRequest with a String. You can tag multiple |
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 WorkRequest
s 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 WorkRequest
s 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
:
- Declare a new variable called
outputWorkInfos
which is aLiveData<List<WorkInfo>>
- In
BlurViewModel
add an init block to get theWorkInfo
usingWorkManager.getWorkInfosByTagLiveData
The code you need is below:
BlurViewModel.kt
// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>
// Add an init block to the BlurViewModel class
init {
// 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:
- Check if the list of
WorkInfo
is not null and if it has anyWorkInfo
objects in it - if not then the Go button has not been clicked yet, so return. - Get the first
WorkInfo
in the list; there will only ever be oneWorkInfo
tagged withTAG_OUTPUT
because we made the chain of work unique. - Check whether the work status is finished, using
workInfo.state().isFinished()
- If it's not finished, then call
showWorkInProgress()
which hides and shows the appropriate views. - If it's finished then call
showWorkFinished()
which hides and shows the appropriate views.
Here's the code:
BlurActivity.kt
// Show work status, added in onCreate()
viewModel.outputWorkInfos.observe(this, workInfosObserver())
// Add this functions
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:
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 the synthetic accessor 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
.
Setup 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
// Put this inside onCreate()
// 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):
- If the
WorkInfo
is finished, get the output data, usingworkInfo.outputData.
- Then get the output URI, remember that it's stored with the
Constants.KEY_IMAGE_URI
key. - Then if the URI isn't empty, it saved properly; show the
outputButton
and callsetOutputUri
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 as String)
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:
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
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!
Last but not least, WorkManager
support Constraints
. For Blur-O-Matic, you'll use the constraint that the device must be charging when it's saving.
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
, as shown below:
BlurViewModel.kt
// Put this inside the applyBlur() function
// 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:
When the device is not charging, it should suspend SaveImageToFileWorker,
executing it only after that you plug it in.
Congratulations! You've finished the Blur-O-Matic app and in the process learned about:
- Adding WorkManager to your Project
- Scheduling a
OneOffWorkRequest
- Input and Output parameters
- Chaining work together
WorkRequest
s - Naming Unique
WorkRequest
chains - Tagging
WorkRequest
s - Displaying
WorkInfo
in the UI - Cancelling
WorkRequest
s - 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 Advance WorkManager codelab.