Join us on the livestream at Android Dev Summit on 7-8 November 2018, starting at 10AM PDT!

Advanced WorkManager topics

WorkManager makes it easy to set up and schedule elaborate task requests. You can use the APIs for scenarios like these:

  • Chained sequences of tasks that run in a specified order
  • Unique named sequences, with rules for what happens if the app launches two sequences with the same name
  • Tasks that pass and return values, including chained tasks where each task passes arguments to the next task in the chain

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():

Kotlin

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()

Java

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.Result.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:

Kotlin

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()

Java

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:

Kotlin

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

Java

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.

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:

Kotlin

// Define the parameter keys:
const val KEY_X_ARG = "X"
const val KEY_Y_ARG = "Y"
const val KEY_Z_ARG = "Z"

// ...and the result key:
const val KEY_RESULT = "result"

// Define the Worker class:
class MathWorker(context : Context, params : WorkerParameters)
    : Worker(context, params)  {

    override fun doWork(): Result {
        val x = inputData.getInt(KEY_X_ARG, 0)
        val y = inputData.getInt(KEY_Y_ARG, 0)
        val z = inputData.getInt(KEY_Z_ARG, 0)

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

        //...set the output, and we're done!
        val output: Data = mapOf(KEY_RESULT to result).toWorkData()
        setOutputData(output)

        return Result.SUCCESS
    }
}

Java

// 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";

    public MathWorker(
        @NonNull Context context,
        @NonNull WorkerParameters params) {
        super(context, params);
    }

    @Override
    public Worker.Result 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 Result.SUCCESS;
    }
}

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

Kotlin

val myData: Data = mapOf("KEY_X_ARG" to 42,
                       "KEY_Y_ARG" to 421,
                       "KEY_Z_ARG" to 8675309)
                     .toWorkData()

// ...then create and enqueue a OneTimeWorkRequest that uses those arguments
val mathWork = OneTimeWorkRequestBuilder<MathWorker>()
        .setInputData(myData)
        .build()
WorkManager.getInstance().enqueue(mathWork)

Java

// 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 mathWork = new OneTimeWorkRequest.Builder(MathWorker.class)
        .setInputData(myData)
        .build();
WorkManager.getInstance().enqueue(mathWork);

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

Kotlin

WorkManager.getInstance().getStatusById(mathWork.id)
        .observe(this, Observer { status ->
            if (status != null && status.state.isFinished) {
                val myResult = status.outputData.getInt(KEY_RESULT,
                      myDefaultValue)
                // ... do something with the result ...
            }
        })

Java

WorkManager.getInstance().getStatusById(mathWork.getId())
    .observe(lifecycleOwner, status -> {
         if (status != null && status.getState().isFinished()) {
           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.

Additional resources

WorkManager is an Android Jetpack architecture component. See it in use in the Sunflower demo app.