Managing work

Once you’ve defined your Worker and your WorkRequest, the last step is to enqueue your work. The simplest way to enqueue work is to call the WorkManager enqueue() method, passing the WorkRequest you want to run.

Kotlin

val myWork: WorkRequest = // ... OneTime or PeriodicWork
WorkManager.getInstance(requireContext()).enqueue(myWork)

Java

WorkRequest myWork = // ... OneTime or PeriodicWork
WorkManager.getInstance(requireContext()).enqueue(myWork);

Use caution when enqueuing work to avoid duplication. For example, an app might try to upload its logs to a backend service every 24 hours. If you aren't careful, you might end up enqueuing the same task many times, even though the job only needs to run once. To achieve this goal, you can schedule the work as unique work.

Unique Work

Unique work is a powerful concept that guarantees that you only have one instance of work with a particular name at a time. Unlike IDs, unique names are human-readable and specified by the developer instead of being auto-generated by WorkManager. Unlike tags, unique names are only associated with a single instance of work.

Unique work can be applied to both one-time and periodic work. You can create a unique work sequence by calling one of these methods, depending on whether you’re scheduling repeating work or one time work.

Both of these methods accept 3 arguments:

  • uniqueWorkName - A String used to uniquely identify the work request.
  • existingWorkPolicy - An enum which tells WorkManager what to do if there's already an unfinished chain of work with that unique name. See conflict resolution policy for more information.
  • work - the WorkRequest to schedule.

Using unique work, we can fix our duplicate scheduling issue noted earlier.

Kotlin

val sendLogsWorkRequest =
       PeriodicWorkRequestBuilder<SendLogsWorker>(24, TimeUnit.HOURS)
           .setConstraints(Constraints.Builder()
               .setRequiresCharging(true)
               .build()
            )
           .build()
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
           "sendLogs",
           ExistingPeriodicWorkPolicy.KEEP,
           sendLogsWorkRequest
)

Java

PeriodicWorkRequest sendLogsWorkRequest = new
      PeriodicWorkRequest.Builder(SendLogsWorker.class, 24, TimeUnit.HOURS)
              .setConstraints(new Constraints.Builder()
              .setRequiresCharging(true)
          .build()
      )
     .build();
WorkManager.getInstance(this).enqueueUniquePeriodicWork(
     "sendLogs",
     ExistingPeriodicWorkPolicy.KEEP,
     sendLogsWorkRequest);

Now, if the code runs while a sendLogs job is already in the queue, the existing job is kept and no new job is added.

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. See Chaining work for more details.

Conflict resolution policy

When scheduling unique work, you must tell WorkManager what action to take when there is a conflict. You do this by passing an enum when enqueuing the work.

For one-time work, you provide an ExistingWorkPolicy, which supports 4 options for handling the conflict.

  • REPLACE existing work with the new work. This option cancels the existing work.
  • KEEP existing work and ignore the new work.
  • APPEND the new work to the end of the existing work. This policy will cause your new work to be chained to the existing work, running after the existing work finishes.

The existing work becomes a prerequisite to the new work. If the existing work becomes CANCELLED or FAILED, the new work is also CANCELLED or FAILED. If you want the new work to run regardless of the status of the existing work, use APPEND_OR_REPLACE instead.

  • APPEND_OR_REPLACE functions similarly to APPEND, except that it is not dependent on prerequisite work status. If the existing work is CANCELLED or FAILED, the new work still runs.

For period work, you provide an ExistingPeriodicWorkPolicy, which supports 2 options, REPLACE and KEEP. These options function the same as their ExistingWorkPolicy counterparts.

Observing your work

At any point after enqueuing work, you can check its status by querying WorkManager by its name, id or by a tag associated with it.

Kotlin

// by id
workManager.getWorkInfoById(syncWorker.id) // ListenableFuture<WorkInfo>

// by name
workManager.getWorkInfosForUniqueWork("sync") // ListenableFuture<List<WorkInfo>>

// by tag
workManager.getWorkInfosByTag("syncTag") // ListenableFuture<List<WorkInfo>>

Java

// by id
workManager.getWorkInfoById(syncWorker.id); // ListenableFuture<WorkInfo>

// by name
workManager.getWorkInfosForUniqueWork("sync"); // ListenableFuture<List<WorkInfo>>

// by tag
workManager.getWorkInfosByTag("syncTag"); // ListenableFuture<List<WorkInfo>>

The query returns a ListenableFuture of a WorkInfo object, which includes the id of the work, its tags, its current State, and any output data set via Result.success(outputData).

A LiveData variant of each of the methods allows you to observe changes to the WorkInfo by registering a listener. For example, if you wanted to display a message to the user when some work finishes successfully, you could set it up as follows:

Kotlin

workManager.getWorkInfoByIdLiveData(syncWorker.id)
               .observe(viewLifecycleOwner) { workInfo ->
   if(workInfo?.state == WorkInfo.State.SUCCEEDED) {
       Snackbar.make(requireView(), 
      R.string.work_completed, Snackbar.LENGTH_SHORT)
           .show()
   }
}

Java

workManager.getWorkInfoByIdLiveData(syncWorker.id)
        .observe(getViewLifecycleOwner(), workInfo -> {
    if (workInfo.getState() != null &&
            workInfo.getState() == WorkInfo.State.SUCCEEDED) {
        Snackbar.make(requireView(),
                    R.string.work_completed, Snackbar.LENGTH_SHORT)
                .show();
   }
});

Complex work queries

WorkManager 2.4.0 and higher supports complex querying for enqueued jobs using WorkQuery objects. WorkQuery supports querying for work by a combination of it’s tag(s), state and unique work name.

The following example shows how you can find all work with the tag, “syncTag”, that is in the FAILED or CANCELLED state and has a unique work name of either “preProcess” or “sync”.

Kotlin

val workQuery = WorkQuery.Builder
       .fromTags(listOf("syncTag"))
       .addStates(listOf(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
       .addUniqueWorkNames(listOf("preProcess", "sync")
    )
   .build()

val workInfos: ListenableFuture<List<WorkInfo>> = workManager.getWorkInfos(workQuery)

Java

WorkQuery workQuery = WorkQuery.Builder
       .fromTags(Arrays.asList("syncTag"))
       .addStates(Arrays.asList(WorkInfo.State.FAILED, WorkInfo.State.CANCELLED))
       .addUniqueWorkNames(Arrays.asList("preProcess", "sync")
     )
    .build();

ListenableFuture<List<WorkInfo>> workInfos = workManager.getWorkInfos(workQuery);

Each component (tag, state, or name) in a WorkQuery is AND-ed with the others. Each value in a component is OR-ed. For example: (name1 OR name2 OR ...) AND (tag1 OR tag2 OR ...) AND (state1 OR state2 OR ...).

WorkQuery also works with the LiveData equivalent, getWorkInfosLiveData().

Cancelling and stopping work

If you no longer need your previously enqueued work to run, you can ask for it to be cancelled. Work can be cancelled by its name, id or by a tag associated with it.

Kotlin

// by id
workManager.cancelWorkById(syncWorker.id)

// by name
workManager.cancelUniqueWork("sync")

// by tag
workManager.cancelAllWorkByTag("syncTag")

Java

// by id
workManager.cancelWorkById(syncWorker.id);

// by name
workManager.cancelUniqueWork("sync");

// by tag
workManager.cancelAllWorkByTag("syncTag");

Under the hood, WorkManager checks the State of the work. If the work is already finished, nothing happens. Otherwise, the work's state is changed to CANCELLED and the work will not run in the future. Any WorkRequest jobs that are dependent on this work will also be CANCELLED.

Currently RUNNING work receives a call to ListenableWorker.onStopped(). Override this method to handle any potential cleanup. See stop a running worker for more information.

Stop a running Worker

There are a few different reasons your running Worker might be stopped by WorkManager:

  • You explicitly asked for it to be cancelled (by calling WorkManager.cancelWorkById(UUID), for example).
  • In the case of unique work, you explicitly enqueued a new WorkRequest with an ExistingWorkPolicy of REPLACE. The old WorkRequest is immediately considered cancelled.
  • Your work's constraints are no longer met.
  • The system instructed your app to stop your work for some reason. This can happen if you exceed the execution deadline of 10 minutes. The work is scheduled for retry at a later time.

Under these conditions, your Worker is stopped.

You should cooperatively abort any work you had in progress and release any resources your Worker is holding onto. For example, you should close open handles to databases and files at this point. There are two mechanisms at your disposal to understand when your Worker is stopping.

onStopped() callback

WorkManager invokes ListenableWorker.onStopped() as soon as your Worker has been stopped. Override this method to close any resources you may be holding onto.

isStopped() property

You can call the ListenableWorker.isStopped() method to check if your worker has already been stopped. If you're performing long-running or repetitive operations in your Worker, you should check this property frequently and use it as a signal for stopping work as soon as possible.

Note: WorkManager ignores the Result set by a Worker that has received the onStop signal, because the Worker is already considered stopped.