Welcome to the second post of our WorkManager series. WorkManager is an Android Jetpack library that runs deferrable, guaranteed background work when the work’s constraints are satisfied. WorkManager is the current best practice for many types of background work. In the first blog post, we talked about what WorkManager is and when to use WorkManager.
In this blog post, I’ll cover:
- Defining your background task as work
- Defining how specific work should run
- Running your work
- Using Chains for dependent work
- Observing your work’s status
I’ll also explain what’s going on behind the scenes with WorkManager, so that you can make informed decisions about how to use it.
Starting with an example
Let’s say you have an image editing app that lets you put filters on images and upload them to the web for the world to see. You want to create a series of background tasks that applies the filters, compresses the images, and then uploads them. In each phase, there is a constraint that needs to be checked — that there is sufficient battery when you are filtering the images, that you have enough storage space when compressing the images, and that you have a network connection when uploading the images.
This is an example of a task that is:
- Deferrable, because you don’t need it to happen immediately, and in fact might want to wait for some constraints to be met (such as waiting for a network connection).
- Needs to be guaranteed to run, regardless of if the app exits, because your users would be pretty unhappy if their filtered images are never shared with the world!
These characteristics make our image filter and uploading tasks a perfect use case for WorkManager.
Adding the WorkManager dependency
The code snippets in this blog post are in Kotlin, using the KTX library (KoTlin eXtensions). The KTX version of the library provides extension functions for more concise and idiomatic Kotlin. You can use the KTX version of WorkManager using this dependency:
def work_version = "1.0.0-beta02"
You can find the latest version of the library here. If you want to use the Java dependency, just remove the “-ktx”.
Define what your work does
Let’s focus on how you execute one piece of work, before we get to chaining multiple tasks together. I’ll zoom in on the upload task. First, you’ll need to create your own implementation of the
Worker class. I’ll call our class
UploadWorker, and override the
- Define what your work actually does.
- Accept inputs and produce outputs. Both inputs and outputs are represented as key, value pairs.
- Always return a value representing success, failure, or retry.
Here’s an example showing how to implement a
Worker that uploads an image:
Two things to note:
- The input and output are passed as
Data, which is essentially a map of primitive types and arrays.
Dataobjects are intended to be fairly small — there’s actually a limit on the total size that can be input/output. This is set by the
MAX_DATA_BYTES. If you need to pass more data in and out of your
Worker, you should put your data elsewhere, such as a Room database. As an example, I’m passing in the URI of the image above, and not the image itself.
- In the code I show two return examples,
Result.failure(). There’s also a
Result.retry()option which will retry your work again at a later time.
Define how your work should run
Worker defines what the work does, a
WorkRequest defines how and when work should be run.
WorkRequest takes in the
imageData: Data object as input and runs as soon as possible.
Let’s say the
UploadWork shouldn’t always just run immediately — it should only run if the device has a network connection. You can do this by adding a
Constraints object. You can create a constraint like this:
Here’s an example of other supported constraints:
Result.retry()? I said earlier that if a
Result.retry(), WorkManager will reschedule the work. You can customize the backoff criteria when you make a new
WorkRequest. This allows you to define when the work should be retried.
The backoff criteria is defined by two properties:
- BackoffPolicy, which by default is exponential, but can be set to linear.
- Duration, which defaults to 30 seconds.
The combined code for enqueuing your upload work, with constraints, input and a custom back-off policy, is:
This is all well and good, but you haven’t actually scheduled your work to run yet. Here’s the one line of code you need to tell WorkManager to schedule your work:
You first need to get the instance of
WorkManager, which is a singleton responsible for executing your work. Calling
enqueue is what starts the whole process of
WorkManager tracking and scheduling work.
Behind the Scenes — How work runs
So what can you expect
WorkManager to do for you? By default,
- Run your work off of the main thread (this assumes you are extending the
Workerclass, as shown above in
- Guarantee your work will execute (it won’t forget to run your work, even if you restart the device or the app exits).
- Run according to best practices for the user’s API level (as described in the previous article).
Let’s explore how WorkManager ensures your work is run off of the main thread and is guaranteed to execute. Behind the scenes, WorkManager includes the following parts:
- Internal TaskExecutor: A single threaded
Executorthat handles all the requests to enqueue work. If you’re not familiar with
Executorsyou can read more about them here.
- WorkManager database: A local database that tracks all of the information and statuses of all of your work. This includes things like the current state of the work, the inputs and outputs to and from the work and any constraints on the work. This database is what enables WorkManager to guarantee your work will finish — if your user’s device restarts and work gets interrupted, all of the details of the work can be pulled from the database and the work can be restarted when the device boots up again.
- WorkerFactory**: A default factory that creates instances of your
Workers. We’ll cover why and how to configure this in a future blog post.
- Default Executor**: A default executor that runs your work unless you specify otherwise. This ensures that by default, your work runs synchronously and off of the main thread.
** These are parts that can be overridden to have different behaviors.
When you enqueue your
- The Internal TaskExecutor immediately saves your
WorkRequestinfo to the WorkManager database.
- Later, when the
WorkRequestare met (which could be immediately), the Internal TaskExecutor tells the
WorkerFactoryto create a
- Then the default
doWork()method off of the main thread.
In this way, your work, by default, is both guaranteed to execute and to run off of the main thread.
Now if you want to use some other mechanism besides the default
Executor to run your work, you can do so! There’s out of the box support for coroutines (
CoroutineWorker) and RxJava (
RxWorker) as means of doing work.
Or you can specify exactly how work is executed by using
Worker is actually an implementation of
ListenableWorker that defaults to running your work on the default
Executor and thus synchronously. So if you want full control over your work’s threading strategy or to run work asynchronously, you can subclass
ListenableWorker (the details of this will be discussed in a later post).
The fact that WorkManager goes to the trouble of saving all of your work’s information into a database is what makes it perfect for tasks that need to be guaranteed to execute. This is also what makes WorkManager overkill for tasks that don’t need that guarantee and just need to be executed on a background thread. For example, let’s say you’ve downloaded an image and you want to change the color of parts of your UI based off of that image. This is work that should be run off of the main thread, but, because it’s directly related to the UI, does not need to continue if you close the app. So in a case like this, don’t use WorkManager — stick with something lighter weight like Kotlin coroutines or creating your own
Using Chains for dependent work
Our filter example included more than just one task — we wanted to filter multiple images, then compress, then upload. If you want to run a series of
WorkRequests, one after the other or in parallel, you can use a chain. The example diagram shows a chain where you have three filter tasks run in parallel, followed by a compress task and an upload task, run in sequence:
This is super easy with WorkManager. Assuming you have created all your WorkRequests with the appropriate constraints, the code looks like:
The three filter-image
WorkRequests execute in parallel. Once all three filter
WorkRequests are finished (and only if all three finish), the
compressWorkRequest happens, followed by the
Another neat feature of chains is that the output of one
WorkRequest is given as input to the next
WorkRequest. So assuming you set your input and output data correctly, like I did above with my
UploadWorker example, these values will get passed along automatically.
Notice that the
InputMerger is added to the
compressWorkRequest, not the three filter requests that are run in parallel.
Let’s assume that the output of each of the filter work requests is the key “KEY_IMAGE_URI” mapped to an image URI. What adding the
ArrayCreatingInputMerger does is it takes the outputs from requests run in parallel and when those outputs have matching keys, it creates an array with all of the output values, mapped to the single key. Visualized this looks like:
So the input to
compressWorkRequest will end up being the pair of “KEY_IMAGE_URI” mapped to an array of filtered image URIs.
Observing your WorkRequest status
getWorkInfoByIdLiveData returns a
WorkInfo includes the output data and an enum representing the state of the work. When the work finishes successfully, its’
SUCCEEDED. So, for example, you could automatically display that image when the work is done by writing some observation code like:
A few things to note:
WorkRequesthas a unique id and that unique id is one way to look up the associated
- The ability to observe and be notified when the
WorkInfochanges is a feature provided by
Work has a lifecycle, represented by different
States. When observing the
LiveData<WorkInfo> you’ll see those states; for example you might see:
The “happy path” of states that work goes through are:
BLOCKED: This state occurs only if the work is in a chain and is not the next work in the chain.
ENQUEUED: Work enters this state as soon as the work is next in the chain of work and eligible to run. This work may still be waiting on
Constraints to be met.
RUNNING: In this state, the work is actively executing. For
Workers, this means the
doWork()method has been called.
SUCCEEDED: Work enters this terminal state when
Now when the work is
RUNNING, you might call
Result.retry(). This will cause the work to go back to
ENQUEUED. The work can also be
CANCELLED at any point.
If the work result is a
Result.failure() instead of a success, its state will end in
FAILED. The full flowchart of states therefore looks like this:
For an excellent video explanation, check out the WorkManager Android Developer Summit talk.
That’s the basics of the WorkManager API. Using the snippets we just covered you can now:
Workers with input and output.
- Configure how your
Workers will run, using
Constraints, starting input and back off policies.
- Understand what
WorkManagerdoes under the hood, by default, in respect to threading and guaranteed execution.
- Create complex chains of interdependent work, running both sequentially and in parallel.
- Observe your
WorkRequests status using
Stay tuned for more blog posts about WorkManager topics as we continue this series. Have a question or something you’d like us to cover? Let us know in the comment section!
Thanks to Pietro Maggi.