paint-brush
Running Background Tasks in Android with WorkManager: Part 1by@azamatnurkhojayev
1,546 reads
1,546 reads

Running Background Tasks in Android with WorkManager: Part 1

by Azamat NurkhojayevApril 11th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Background work is an important part of an application. WorkManager allows you to run background tasks sequentially or in parallel. You can transfer data to them, monitor the execution status and run only if the specified conditions are met. Let's create and run a background task.
featured image - Running Background Tasks in Android with WorkManager: Part 1
Azamat Nurkhojayev HackerNoon profile picture


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.


Let's create and run a background task

  1. Add to dependencies:
// (for Java only)    
implementation("androidx.work:work-runtime:$work_version")

or

// for Kotlin + coroutines    
implementation("androidx.work:work-runtime-ktx:$work_version")


  1. Create a class that inherits the 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.


  1. Now we need to wrap 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.


  1. Now you can run the task:
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.

Task Status

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.

State

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.


Cancel a task

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.


Tag

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


setInitialDelay

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.


Periodic task

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.