Android WorkManager

Android WorkManager
Photo by Mike Alonzo / Unsplash

Google announced the WorkManager library at I/O 2018 and in this month's post we'll explore the API and see how we can use it to handle some of the most common background work scenarios. You can see code examples in this post's companion project here

Doing work in the background on android has always been a complicated matter. Before WorkManager you needed to be familiar with JobScheduler, Firebase JobDispatcher, and AlarmManager. You also needed to know when to employ each of these options based on the background work that needed to be done and even the device's API level. WorkManager aims to eliminate a lot of that mental overhead by giving us a single place to go when we need to schedule some background work in our app. It is important though to understand that the WorkManager library is still not a silver bullet for all your background work related activities. You'll still need to make sure you understand the background execution limits placed on app's starting with Oreo. This page is a helpful resource to refer to when you need to decide what you should use for your background task. Workmanager will be your goto API for when work is deferrable and has to finish.

Let's start our tour of WorkManager by looking at some of the key objects in the library and the purpose that they serve.


Worker: Specifies the task that you need to perform, the library includes an abstract class that you can extend. The magic will happen in this class's doWork method. This method will be called on a background thread for you.

WorkRequest: represents an individual task, at a minimum it specifies what worker class should perform the request's work, however, you can also specify job conditions and circumstances. Every request has an auto generated ID that can be used to cancel or look up the status of the work. The library provides OneTimeWorkRequest and PeriodicWorkRequest

WorkRequest.Builder: This class will help you build your work requests.

Constraints: The restrictions you would like to place on a given WorkRequest.

Constraints.Builder: Another helpful builder class that will create Constraint objects for you.

WorkContinuation: This class makes it easy to combine different workers together. These worker chains can then be executed sequentially or in parrallel.

WorkManager: This object enqueues and manages your work requests.

WorkStatus: Contains the important information about a given work request.
Can be returned as a LiveData so that you can easily update your UI as the work progresses.


Now that we're familiar with the library's basic API lets dive into some examples of how we can put it to use. In our first scenario we'll have some work that we need the app to execute periodically. This could be something like a simple sync job that needs to send a small amount of data to our app's backend. Our requirements are that the job runs once a day and it needs the network to be connected. We also don't want our job to put the user in a bad situation so we'll also say we don't want our job to run if their battery is low. We also do not want this job to be duplicated if it has already been scheduled. Let's see how the WorkManager library can make help us schedule such a task in about 10 lines of code.

First let's see what our Worker class might look like

class OfflineSyncWorker : Worker() {

    override fun doWork(): Result {
        Log.d("OfflineSyncWorker", "doing work...")

        //Pretend to do some work
        val random = Random(System.currentTimeMillis())
        val randomJobTime = (random.nextInt(15) + 5) * 1000

        Thread.sleep(randomJobTime.toLong())

        Log.d("OfflineSyncWorker", "Job's Done!")
        return Result.SUCCESS
    }

    companion object {
        val TAG = "offlineSyncWorker"
    }
}

Now let's look at how we can schedule that worker to do his work.


private fun schedulePeriodicOfflineWorkSync() {
        val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .setRequiresBatteryNotLow(true)
                .build()

        val syncRequest = PeriodicWorkRequest.Builder(OfflineSyncWorker::class.java, 
                            24L, 
                            TimeUnit.HOURS)
                .setConstraints(constraints)
                .build()

        workManager.enqueueUniquePeriodicWork(OfflineSyncWorker.TAG, ExistingPeriodicWorkPolicy.REPLACE, syncRequest)
    }


The first thing our code will do is create some constraints. We use the Constraints.Builder object to create a constraints object that will tell the work manager we don't want this job to run if the battery is low and we require that the device have some kind of network connection.

Once we have our constraints we can create our work request. We'll be using the PeriodicWorkRequest.Builder class because we want our task to run daily rather than as a one off task. We specify the worker class that executes the actual code we need to run in order to sync our data, as well as how often we want our job to run. Finally we will add our constraints object to the work request and then build it.

At this point we have a work request and all we need to do is enqueue it with our work manager. The work manager API includes the idea of "unique" work. In our example we might schedule this job as soon as our app launches but we wouldn't want to schedule a new job every single time the user launches our app or we may end up with hundreds of these tasks all executing once a day. This is where the idea of "unique" work can help us. When we enqueue our work with the WorkManager we will use the enqueueUniquePeriodicWork method.
This method will let us specify a tag, an existing work policy, and the work request. The tag can be used to retrieve the status of our work later, or even to cancel the job. The existing work policy let's us tell the work manager what we want to do with any previously scheduled unique work with that same tag. We can tell the work manager to cancel and delete any previously scheduled work and then insert our latest work request, or we can tell the work manager to keep the pending work and ignore our new request.

That's all we need to do to schedule some periodic work with the WorkManager API. It's worth mentioning that there are some additional constraints for periodic work. The first one being that you cant schedule periodic work with a period smaller than 15 minutes. Your periodic work will still be subject to the doze mode restrictions and it can not be chained or delayed.

In our next scenario we'll look at an example of a simple one time work request. This work will be kicked off when the user clicks a button in our app.

val constraints = Constraints.Builder()
                .setRequiredNetworkType(NetworkType.CONNECTED)
                .build()

        val uploadWork = OneTimeWorkRequest.Builder(SimpleUploadWorker::class.java)
                .setConstraints(constraints)
                .build()

        workManager.enqueue(uploadWork)

This simple job will only have a single constraint. We'll require a network connection in order for it to execute. We won't require any additional constraints like battery health since the user has initiated this job via some action on our UI.

We'll use the OneTimeWorkRequest.Builder to create our work request. It'll just need to know which worker class to use and what constraint object to apply before we build it. After that we simply call enqueue on our work manager. This is probably the simplest example of how to do some work in the background using WorkManager and it's really great at illustrating how easy the architecture components team has made this previously onerous task.

In our last example we'll look at how you can chain multiple worker tasks together. We'll also use this example to show how we can monitor the status of our worker's with LiveData. Before we can build a chain of work requests we'll need multiple work requests. In our example we simply define two one time work requests each with a different worker class.

val stageOneWorker = OneTimeWorkRequest.Builder(PreCalculationWorker::class.java)
                .build()

        val stageTwoWorker = OneTimeWorkRequest.Builder(ComplicatedCalculationWorker::class.java)
                .build()

Chaining these two work requests together is about as simple as it can be with the WorkManager API

        workManager.beginUniqueWork("Complicated_Analysis_Chain_Work", ExistingWorkPolicy.KEEP, stageOneWorker)
                .then(stageTwoWorker).enqueue()

First we call beginUniqueWork and give our work chain a unique tag as well as the KEEP existing work policy. We do this because we don't want our "complicated" chain of work to kick off a second time if its already been queued up. We then pass the first stage's worker request as the final parameter. The workmanager will then return a WorkContinuation object that we can call then() on and pass in our second worker request.
Finally we call enqueue() to get our chain of work enqueued in our workmanager.

If stage one of our chained work fails for some reason then stage two will never execute. In our example we just chained two work requests together sequentially but the WorkManager API also allows you chain worker objects together in more complex ways. You could even schedule several workers to run in parallel and then chain to them a final completion stage that executes when they are done. All of this is made possible by the WorkContinuation object.

Let's pretend that back in our activity or fragment we'd like to monitor our work chain's progress.

val statusesForUniqueWork = workManager.getStatusesForUniqueWork("Complicated_Analysis_Chain_Work")

statusesForUniqueWork.observe(this, Observer { workerStatusList ->
                //Loop over the worker status list to determine
                //what stages are running and what stages are completed.
})

We first need to just ask the workmanager for the status via getStatusesForUniqueWork and pass it our worker chain's tag. The work manager will then return a live data list to us. Each object in that list will be a WorkStatus object that we can query for additional information about each task in the chain.

In addition to the status of a given worker, the WorkStatus object can also contain "outputData". Worker objects in the library can set an instance of the Data class as their output when they have finished their task. This class is a simple key-value pair container designed to hold a very small amount of data. Its important to keep in mind that the Data class is not designed to hold a large list or tree of objects as your worker's output. Ideally you would store the raw data somewhere else (maybe in sqlite using Room) and then just put keys or identifiers into the worker's output data object.

In addition to setting a Data object as their output worker objects can also be sent data via input when the work request is created. For example if we wanted to give some information to our stage one worker in our worker chain example it might look something like this


val inputData: Data = mapOf("someImportantKey", 42).toWorkData()

val stageOneWorker = OneTimeWorkRequest.Builder(PreCalculationWorker::class.java)
                .setInputData(inputData)
                .build()

Then inside of our worker's doWork method we could get that key like this:


override fun doWork(): Result {
    val importantKey = inputData.getInt("someImportantKey")

//do work down here

}

That concludes our introduction to the WorkManager library. As of this post's writing the library was in the alpha06 release so things may still change a little but I'm sure the basic concepts will look very similar if not the same by the time 1.0 is released. I'll be sure to write an update if anything major changes.

Helpful Links


Example Project

WorkManager Guide

Android Background Work Guide

Android Background Execution Limits I/O Talk