WorkManager basics
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:
dependencies {
def work_version = "1.0.0-beta02"
implementation "android.arch.work:work-runtime-ktx:$work_version"
}
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 doWork()
method.
Worker
s:
- 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.Data
objects are intended to be fairly small — there’s actually a limit on the total size that can be input/output. This is set by theMAX_DATA_BYTES
. If you need to pass more data in and out of yourWorker
, 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.success()
andResult.failure()
. There’s also aResult.retry()
option which will retry your work again at a later time.
Define how your work should run
While a Worker
defines what the work does, a WorkRequest
defines how and when work should be run.
Here’s an example of creating a OneTimeWorkRequest
for your UploadWorker
. It is also possible to have a repeating PeriodicWorkRequest
:
This 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:
Finally, remember Result.retry()
? I said earlier that if a Worker
returns 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:
Running work
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, WorkManager
will:
- Run your work off of the main thread (this assumes you are extending the
Worker
class, as shown above inUploadWorker
). - 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
Executor
that handles all the requests to enqueue work. If you’re not familiar withExecutors
you 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
Worker
s. 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 WorkRequest
:
- The Internal TaskExecutor immediately saves your
WorkRequest
info to the WorkManager database. - Later, when the
Constraints
for theWorkRequest
are met (which could be immediately), the Internal TaskExecutor tells theWorkerFactory
to create aWorker
. - Then the default
Executor
calls yourWorker
’sdoWork()
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 ListenableWorker
. 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 Executor
.
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 WorkRequest
s, 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 WorkRequest
s execute in parallel. Once all three filter WorkRequests
are finished (and only if all three finish), the compressWorkRequest
happens, followed by the uploadWorkRequest
.
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.
For handling output from the three filter work requests run parallel, you can use an InputMerger
, specifically the ArrayCreatingInputMerger
. This looks like:
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
The easiest way to observe work is by using the LiveData
class. If you’re not familiar with LiveData
, it’s a lifecycle-aware observable data holder — and it’s described in more detail here.
Calling getWorkInfoByIdLiveData
returns a LiveData
of WorkInfo
. WorkInfo
includes the output data and an enum representing the state of the work. When the work finishes successfully, its’ State
is 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:
- Each
WorkRequest
has a unique id and that unique id is one way to look up the associatedWorkInfo
. - The ability to observe and be notified when the
WorkInfo
changes is a feature provided byLiveData
.
Work has a lifecycle, represented by different State
s. 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 onConstraint
s to be met.RUNNING
: In this state, the work is actively executing. ForWorker
s, this means thedoWork()
method has been called.SUCCEEDED
: Work enters this terminal state whendoWork()
returnsResult.success()
.
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.
Conclusion
That’s the basics of the WorkManager API. Using the snippets we just covered you can now:
- Create
Worker
s with input and output. - Configure how your
Worker
s will run, usingWorkRequest
s,Constraint
s, starting input and back off policies. - Enqueue
WorkRequest
s. - Understand what
WorkManager
does 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
WorkRequest
s status usingWorkInfo
.
Want to try WorkManager yourself? Check out the codelab, which is in both Kotlin and Java.
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.