Schedule tasks with WorkManager

The WorkManager API makes it easy to specify deferrable, asynchronous tasks and when they should run. These APIs let you create a task and hand it off to WorkManager to run immediately or at an appropriate time. For example, an app might need to download new resources from the network from time to time. Using these classes, you can set up a task, choose appropriate circumstances for it to run (like "only while device is charging and online"), and hand it off to WorkManager to run when the conditions are met. The task is still guaranteed to run, even if your app is force-quit or the device is rebooted.

WorkManager chooses the appropriate way to run your task based on such factors as the device API level and the app state. If WorkManager executes one of your tasks while the app is running, WorkManager can run your task in a new thread in your app's process. If your app is not running, WorkManager chooses an appropriate way to schedule a background task--depending on the device API level and included dependencies, WorkManager might use JobScheduler, Firebase JobDispatcher, or AlarmManager. You don't need to write device logic to figure out what capabilities the device has and choose an appropriate API; instead, you can just hand your task off to WorkManager and let it choose the best option.

In addition, WorkManager provides several advanced features. For example, you can set up a chain of tasks; when one task finishes, WorkManager queues up the next task in the chain. You can also check a task's state and its return values by observing its LiveData; this can be useful if you want to show UI indicating your task's status.

This overview covers the most important WorkManager features. However, there are many more features available; for full details, see the WorkManager reference documentation.

Classes and concepts

The WorkManager API uses several different classes. In some cases, you'll need to subclass one of the API classes.

The most important classes are:

Typical workflow

Suppose that you're writing a photo library app, and that app needs to periodically compress its stored images. You want to use the WorkManager APIs to schedule the image compression. In this case, you don't particularly care when the compression happens; you want to set up the task and forget about it.

First, you would define your Worker class, and override its doWork() method. Your worker class specifies how to perform the operation, but doesn't have any information about when the task should run.

public class CompressWorker extends Worker {
    @Override
    public Worker.WorkerResult doWork() {

        // Do the work here--in this case, compress the stored images.
        // In this example no parameters are passed; the task is
        // assumed to be "compress the whole library."
        myCompress();

        // Indicate success or failure with your return value:
        return WorkerResult.SUCCESS;

        // (Returning RETRY tells WorkManager to try this task again
        // later; FAILURE says not to try again.)
    }
}

Next, you create a OneTimeWorkRequest object based on that Worker, then enqueue the task with WorkManager:

OneTimeWorkRequest compressionWork =
        new OneTimeWorkRequest.Builder(CompressWorker.class)
    .build();
WorkManager.getInstance().enqueue(compressionWork);

WorkManager chooses an appropriate time to run the task, balancing such considerations as the load on the system, whether the device is plugged in, and so on. In most cases, if you don't specify any constraints, WorkManager runs your task right away. If you need to check on the task status, you can get a WorkStatus object by getting a handle to the appropriate LiveData<WorkStatus>. For example, if you want to check if the task has finished, you could use code like this:

WorkManager.getInstance().getStatusById(compressionWork.getId())
    .observe(lifecycleOwner, workStatus -> {
        // Do something with the status
        if (workStatus != null && workStatus.getState().isFinished())
        { ... }
    });

For more information about working with LiveData, see the LiveData overview.

Task constraints

If you wish, you can specify constraints on when the task should run. For example, you might want to specify that the task should only run when the device is idle, and connected to power. In this case, you'd need to create a OneTimeWorkRequest.Builder object, and use that builder to create the actual OneTimeWorkRequest:

// Create a Constraints that defines when the task should run
Constraints myConstraints = new Constraints.Builder()
    .setRequiresDeviceIdle(true)
    .setRequiresCharging(true)
    // Many other constraints are available, see the
    // Constraints.Builder reference
     .build();

// ...then create a OneTimeWorkRequest that uses those constraints
OneTimeWorkRequest compressionWork =
                new OneTimeWorkRequest.Builder(CompressWorker.class)
     .setConstraints(myConstraints)
     .build();

Then pass the new OneTimeWorkRequest object to WorkManager.enqueue(), as before. WorkManager considers your constraints when finding a time to run the task.

Canceling a Task

You can cancel a task after you enqueue it. To cancel the task, you need its work ID, which you can get from the WorkRequest object. For example, the following code cancels the compressionWork request from the previous section:

UUID compressionWorkId = compressionWork.getId();
WorkManager.getInstance().cancelByWorkId(compressionWorkId);

WorkManager makes its best effort to cancel the task, but this is inherently uncertain--the task may already be running or finished when you attempt to cancel it. WorkManager also provides methods to cancel all tasks in a unique work sequence, or all tasks with a specified tag, also on a best-effort basis.

Advanced functionality

The core functionality of the WorkManager API enables you to create simple, fire-and-forget tasks. Beyond this, the API offers advances features that let you set up more elaborate requests.

Recurring tasks

You might have a task that you need to perform repeatedly. For example, the photo manager app wouldn't want to compress its photos just once. More likely, it would want to examine its shared photos every so often, and see if there are any new or changed images that need to be compressed. This recurring task could compress the images it finds, or alternatively, it could fire off new "compress this image" tasks.

To create a recurring task, use the PeriodicWorkRequest.Builder class to create a PeriodicWorkRequest object, then enqueue the PeriodicWorkRequest the same way you would a OneTimeWorkRequest object. For example, suppose we defined an PhotoCheckWorker class to identify images that need to be compressed. If we want to run the inventory task every 12 hours, we would create a PeriodicWorkRequest object like this:

new PeriodicWorkRequest.Builder photoWorkBuilder =
        new PeriodicWorkRequest.Builder(PhotoCheckWorker.class, 12,
                TimeUnit.HOURS);
// ...if you want, you can apply constraints to the builder here...

// Create the actual work object:
PeriodicWorkRequest photoWork = photoWorkBuilder.build();
// Then enqueue the recurring task:
WorkManager.getInstance().enqueue(invWork);

The WorkManager attempts to run your task at the interval you request, subject to the constraints you impose and its other requirements.

Chained tasks

Your app might need to run several tasks in a particular order. WorkManager allows you to create and enqueue a work sequence that specifies multiple tasks, and what order they should run in.

For example, suppose your app has three OneTimeWorkRequest objects: workA, workB, and workC. The tasks must be run in that order. To enqueue them, create a sequence with the WorkManager.beginWith() method, passing the first OneTimeWorkRequest object; that method returns a WorkContinuation object, which defines a sequence of tasks. Then add the remaining OneTimeWorkRequest objects, in order, with WorkContinuation.then(), and finally, enqueue the whole sequence with WorkContinuation.enqueue():

WorkManager.getInstance()
    .beginWith(workA)
        // Note: WorkManager.beginWith() returns a
        // WorkContinuation object; the following calls are
        // to WorkContinuation methods
    .then(workB)    // FYI, then() returns a new WorkContinuation instance
    .then(workC)
    .enqueue();

The WorkManager runs the tasks in the requested order, according to each task's specified constraints. If any task returns Worker.WorkerResult.FAILURE, the whole sequence ends.

You can also pass multiple OneTimeWorkRequest objects to any of the beginWith() and .then() calls. If you pass several OneTimeWorkRequest objects to a single method call, the WorkManager runs all of those tasks (in parallel) before it runs the rest of the sequence. For example:

WorkManager.getInstance()
    // First, run all the A tasks (in parallel):
    .beginWith(workA1, workA2, workA3)
    // ...when all A tasks are finished, run the single B task:
    .then(workB)
    // ...then run the C tasks (in any order):
    .then(workC1, workC2)
    .enqueue();

You can create more complex sequences by joining multiple chains with the WorkContinuation.combine() methods. For example, suppose you want to run a sequence like this:

Figure 1. You can use WorkContinuation to set up complex chained tasks.

To set up this sequence, create two separate chains, then join them together into a third one:

WorkContinuation chain1 = WorkManager.getInstance()
    .beginWith(workA)
    .then(workB);
WorkContinuation chain2 = WorkManager.getInstance()
    .beginWith(workC)
    .then(workD);
WorkContinuation chain3 = WorkContinuation
    .combine(chain1, chain2)
    .then(workE);
chain3.enqueue();

In this case, WorkManager runs workA before workB. It also runs workC before workD. After both workB and workD have finished, WorkManager runs workE.

There are a number of variants of the WorkContinuation methods that provide shorthands for particular situations. For example, there's a WorkContinuation.combine(OneTimeWorkRequest, WorkContinuation…) method, which instructs the WorkManager to complete all the specified WorkContinuation chains, then finish up with the specified OneTimeWorkRequest. For details, see the WorkContinuation reference.

Unique work sequences

You can create a unique work sequence, by beginning the sequence with a call to beginUniqueWork() instead of beginWith(). Each unique work sequence has a name; the WorkManager only permits one work sequence with that name at a time. When you create a new unique work sequence, you specify what WorkManager should do if there's already an unfinished sequence with the same name:

  • Cancel the existing sequence and replace it with the new one
  • Keep the existing sequence and ignore your new request
  • Append your new sequence to the existing one, running the new sequence's first task after the existing sequence's last task finishes

Unique work sequences can be useful if you have a task that shouldn't be enqueued multiple times. For example, if your app needs to sync its data to the network, you might enqueue a sequence named "sync", and specify that your new task should be ignored if there's already a sequence with that name. Unique work sequences can also be useful if you need to gradually build up a long chain of tasks. For example, a photo editing app might let users undo a long chain of actions. Each of those undo operations might take a while, but they have to be performed in the correct order. In this case, the app could create an "undo" chain and append each undo operation to the chain as needed.

Tagged work

You can group your tasks logically by assigning a tag string to any WorkRequest object. To set a tag, call WorkRequest.Builder.addTag(), for example:

OneTimeWorkRequest cacheCleanupTask =
        new OneTimeWorkRequest.Builder(MyCacheCleanupWorker.class)
    .setConstraints(myConstraints)
    .addTag("cleanup")
    .build();

The WorkManager classes provide several utility methods that let you operate on all tasks with a particular tag. For example, WorkManager.cancelAllWorkByTag() cancels all tasks with a particular tag, and WorkManager.getStatusesByTag() returns a list of all the WorkStatus for all tasks with that tag.

Input parameters and returned values

For greater flexibility, you can pass arguments to your tasks and have the tasks return results. Passed and returned values are key-value pairs. To pass an argument to a task, call the WorkRequest.Builder.setInputData() method before you create the WorkRequest object. That method takes a Data object, which you create with Data.Builder. The Worker class can access those arguments by calling Worker.getInputData(). To output a return value, the task calls Worker.setOutputData(), which takes a Data object; you can get the output by observing the task's LiveData<WorkStatus>.

For example, suppose you have a Worker class that performs a time-consuming calculation. The following code shows what the Worker class would look like:

// Define the Worker class:
public class MathWorker extends Worker {

    // Define the parameter keys:
    public static final String KEY_X_ARG = "X";
    public static final String KEY_Y_ARG = "Y";
    public static final String KEY_Z_ARG = "Z";
    // ...and the result key:
    public static final String KEY_RESULT = "result";

    @Override
    public Worker.WorkerResult doWork() {


        // Fetch the arguments (and specify default values):
        int x = getInputData().getInt(KEY_X_ARG, 0);
        int y = getInputData().getInt(KEY_Y_ARG, 0);
        int z = getInputData().getInt(KEY_Z_ARG, 0);

        // ...do the math...
        int result = myCrazyMathFunction(x, y, z);

        //...set the output, and we're done!
        Data output = new Data.Builder()
            .putInt(KEY_RESULT, result)
            .build();
        setOutputData(output);
        return WorkerResult.SUCCESS;
    }
}

To create the work and pass the arguments, you'd use code like this:

// Create the Data object:
Data myData = new Data.Builder()
    // We need to pass three integers: X, Y, and Z
    .putInt(KEY_X_ARG, 42)
    .putInt(KEY_Y_ARG, 421)
    .putInt(KEY_Z_ARG, 8675309)
    // ... and build the actual Data object:
    .build();

// ...then create and enqueue a OneTimeWorkRequest that uses those arguments
OneTimeWorkRequest.Builder argsWorkBuilder =
          new OneTimeWorkRequest.Builder(MathWorker.class)
    .setInputData(myData);
OneTimeWorkRequest mathWork = argsWorkBuilder.build();
WorkManager.getInstance().enqueue(mathWork);

The returned value would be available in the task's WorkStatus:

WorkManager.getInstance().getStatusById(mathWork.getId())
    .observe(lifecycleOwner, status -> {
         if (status != null) {
           int myResult =
               status.getOutputData().getInt(KEY_RESULT,
                  myDefaultValue));
    // ... do something with the result ...
         }
    });

If you chain tasks, the outputs from one task are available as inputs to the next task in the chain. If it's a simple chain, with a single OneTimeWorkRequest followed by another single OneTimeWorkRequest, the first task returns its result by calling setOutputData(), and the next task fetches that result by calling getInputData(). If the chain is more complicated—for example, because several tasks all send output to a single following task—you can define an InputMerger on the OneTimeWorkRequest.Builder to specify what should happen if different tasks return an output with the same key.