An important part of an application's tasks is background work. It can be downloaded or uploaded, compressed or decompressed, synchronized, etc. Once upon a time, services were designed for background work. But in Android 8, they were very limited: if the application is not active, then the service will be stopped after some time. And long before Android 8, developers started using tools like JobScheduler or Firebase JobDispatcher to run background tasks.
WorkManager allows you to run background tasks sequentially or in parallel, transfer data to them, get results from them, monitor the execution status, and run only if the specified conditions are met.
It is actually very easy to use.
// (for Java only)
implementation("androidx.work:work-runtime:$work_version")
or
// for Kotlin + coroutines
implementation("androidx.work:work-runtime-ktx:$work_version")
Worker
class:class MyWorker(context: Context, parameters: WorkerParameters) : Worker(context, parameters) {
private val TAG = this.javaClass.simpleName
override fun doWork(): Result {
Log.d(TAG, "doWork: start")
try {
TimeUnit.SECONDS.sleep(10)
} catch (e: InterruptedException) {
e.printStackTrace()
}
Log.d(TAG, "doWork: end")
return Result.success()
}
}
In the doWork
method, we are invited to place the code that will be executed. Here I just pause for 10 seconds and return Result.success
, which means that everything was successful. We don't need to bother with threads, because the code will be executed on a non-UI thread.
MyWorker
in a WorkRequest
:val workRequest: WorkRequest = OneTimeWorkRequestBuilder<MyWorker>().build()
WorkRequest
allows us to set start conditions and input parameters to the task. So far, we are not setting anything, but simply creating a OneTimeWorkRequestBuilder
, to which we say that the MyWorker
task will need to be launched.
OneTimeWorkRequestBuilder
has such a name for a reason. This task will be performed once. There is also a PeriodicWorkRequest
, but more on that later.
WorkManager
.getInstance(this)
.enqueue(workRequest)
We take WorkManager
and pass WorkRequest
to its enqueue method. After that, the task will be started.
Let's look at the log:
2023-04-02 10:01:58.364 20001-20040 MyWorker D doWork: start
2023-04-02 10:02:08.367 20001-20040 MyWorker D doWork: end
It can be seen that the task was running for 10 seconds, and the code was not running on the UI thread.
WorkManager provides the ability to track the status of a task. For example, in Activity, we write:
WorkManager.getInstance(this)
.getWorkInfoByIdLiveData(workRequest.id)
.observe(this) { workInfo ->
Log.d(TAG, "onChanged: " + workInfo.state)
}
The getWorkInfoByIdLiveData method must pass the task ID, which can be obtained by the
WorkRequest.id
method. As a state, we receive LiveData, subscribe to it, and all changes in the status of our task will come to the onChanged
method. Using the WorkInfo.state method we will get the current state.
We launch:
2023-04-02 10:41:26.492 25791-25791 MainActivity D onChanged: ENQUEUED
2023-04-02 10:41:26.530 25791-25826 MyWorker D doWork: start
2023-04-02 10:41:26.531 25791-25791 MainActivity D onChanged: RUNNING
2023-04-02 10:41:36.531 25791-25826 MyWorker D doWork: end
2023-04-02 10:41:36.560 25791-25791 MainActivity D onChanged: SUCCEEDED
Immediately after calling the enqueue method, the task is in the ENQUEUED status. The WorkManager then determines that the task can be run, and executes our code. At this point, the status changes to RUNNING. After execution, the status will be SUCCEEDED, because we returned such a status in the doWork
method. The status comes to us in the UI thread.
Now let's run the task again and close the application:
2023-04-02 10:50:38.057 25791-25791 MainActivity D onChanged: ENQUEUED
2023-04-02 10:50:38.067 25791-25923 MyWorker D doWork: start
2023-04-02 10:50:38.071 25791-25791 MainActivity D onChanged: RUNNING
2023-04-02 10:50:48.073 25791-25923 MyWorker D doWork: end
Please note that the task has been completed, but the SUCCEEDED status has not arrived. Why? Because, by closing the Activity, we just unsubscribed from LiveData, which passed us the status of the task. But the task itself has not gone away. It does not depend on the application in any way and will be executed even if the application is closed.
In our task, we returned the WorkInfo.SUCCESS status, thereby reporting that everything is ok. There are two more options:
FAILURE - in this case, after the task is completed, workInfo.state will return FAILED. For us, this is a signal that the task was not completed.
CANCELLED - used to indicate that the task has been canceled and will not execute. All dependent work will also be marked as CANCELLED and will not run.
BLOCKED - the task is currently blocked because its prerequisites haven't been finished successfully.
We can cancel the task with the cancelWorkById
method by passing the task ID:
WorkManager.getInstance(this).cancelWorkById(workRequest.id)
This will call the onStopped
method in the MyWorker
class (if you have implemented it). Also, in the MyWorker
class, we can always use the boolean isStopped
method to check if the task has been canceled.
If we track the status of a task, then WorkInfo.state will return Cancelled. There is also a cancelAllWork
method that will cancel all your tasks. But the help warns that it is extremely undesirable for use; it may hook the libraries you are using.
You can assign a tag to a task using the addTag
method:
val workRequest: WorkRequest = OneTimeWorkRequestBuilder<MyWorker>()
.addTag("myWorkTag")
.build()
You can add multiple tags to one task. WorkInfo has a getTags
method that will return all the tags assigned to this task.
By assigning one tag to several tasks, we can cancel all of them using the cancelAllWorkByTag
method:
WorkManager.getInstance(this).cancelAllWorkByTag("myWorkTag")
Task execution can be postponed for a specified time.
In the setInitialDelay method, we indicated that the task should be started 10 seconds after it is passed to WorkManager.enqueue
.
The OneTimeWorkRequestBuilder
we've reviewed is a one-time task. And if you need multiple executions after a certain period of time, then you can use PeriodicWorkRequestBuilder
:
val periodicWorkRequest: WorkRequest = PeriodicWorkRequestBuilder<MyWorker>(30, TimeUnit.MINUTES)
.build()
In the builder, set the interval to 30 minutes. The task will now run at that interval.
The minimum available interval is 15 minutes. If you set it to less, WorkManager will automatically increase it to 15 minutes.
The WorkManager guarantees that the task will run once during the specified interval. And this can happen at any point in the interval - after 1 minute, after 10, or after 29. With the flex parameter, you can limit the allowed start time range.
val periodicWorkRequest: WorkRequest = PeriodicWorkRequestBuilder<MyWorker>(30, TimeUnit.MINUTES, 25, TimeUnit.MINUTES)
.build()
In addition to the interval of 30 minutes, we additionally pass the parameter 25 minutes to the flex builder. Now the task will not be launched at any moment of the 30-minute interval, but only after the 25th minute. Those between 25 and 30 minutes.