Running Background Tasks in Android with WorkManager: Part 3

Written by azamatnurkhojayev | Published 2023/05/03
Tech Story Tags: android | android-app-development | android-mobile-app | android-jetpack | android-apps | androiddev | android-tutorial | workmanager

TLDRThis tutorial will look at how to run tasks in a specific order. For example, there is a task - to download an archive with files, unpack it, and process the files. This can be done with three successive tasks: download archive, unpacking the archive, and file processing.via the TL;DR App

Previous article in this series: Running Background Tasks in Android with WorkManager: Part 2

Sequence of Tasks

This tutorial will explore methods for running tasks in specific sequential orders.

For example, say there you have a task where you want to download an archive with files, unpack it, and then process the files.

This can be done with three successive tasks:

  • download archive
  • unpacking the archive
  • file processing

Let's see how we can run these tasks sequentially

First, let's make sure that tasks started in the usual way will be executed in parallel. We start three tasks at once:

val worker1: WorkRequest = OneTimeWorkRequestBuilder<Worker1>()
    .build()  

val worker2: WorkRequest = OneTimeWorkRequestBuilder<Worker2>()  
    .build()  

val worker3: WorkRequest = OneTimeWorkRequestBuilder<Worker3>()  
    .build()  

WorkManager  
    .getInstance(this)  
    .enqueue(listOf(worker1, worker2, worker3))

We look at the log:

2023-04-18 21:07:05 18984-19015 Worker2  D  doWork: start
2023-04-18 21:07:05 18984-19016 Worker3  D  doWork: start
2023-04-18 21:07:05 18984-19014 Worker1  D  doWork: start

2023-04-18 21:07:06 18984-19014 Worker1  D  doWork: end
2023-04-18 21:07:06 18984-19016 Worker3  D  doWork: end
2023-04-18 21:07:06 18984-19015 Worker2  D  doWork: end

The tasks started at  21:07:05 and were executed in parallel in different threads, and each was completed on its own time.

We saw parallel execution. Now let's execute them sequentially. We pass the first task to the beginWith method and thereby create the beginning of the task sequence. Next, by calling the then method, we add the second and third tasks to the sequence. And using the enqueue method, we send this sequence to launch.

Result:

2023-04-18 21:28:59 20007-20036 Worker1  D  doWork: start
2023-04-18 21:29:00 20007-20036 Worker1  D  doWork: end
2023-04-18 21:29:00 20007-20044 Worker2  D  doWork: start
2023-04-18 21:29:01 20007-20044 Worker2  D  doWork: end
2023-04-18 21:29:01 20007-20045 Worker3  D  doWork: start
2023-04-18 21:29:02 20007-20045 Worker3  D  doWork: end

The logs show that the tasks were performed one after another, and in the exact sequence that we indicated.

How will the criteria affect the execution of the task sequence?

A task that cannot currently be started will wait. And, accordingly, all other tasks that are in the sequence after this task will also wait.

Let's look at an example.

Let the second task have a criterion - the presence of the Internet. Turn off the Internet on the device and start the sequence. The first task doesn't care; it's running. The turn of the second task comes, but there is no Internet; so the second task is put on hold, and the third task can be started only after the completion of the second one. So it has to wait. We turn on the Internet, the second task is performed, and after it, the third one is performed.

If any task in the sequence ends with a FAILURE status, the entire chain will be stopped.

We can combine serial and parallel execution of tasks.

WorkManager  
    .getInstance(this)  
    .beginWith(listOf(worker1, worker2))  
    .then(listOf(worker3, worker4))  
    .then(worker5)  
    .enqueue()

Here we form a sequence, but at the same time, we specify two tasks for the first (beginWith) and second (first then) step of the sequence.

As a result, the tasks worker1 and worker2 will be executed first, and they will be executed in parallel. After that, worker3 and worker4 will be executed, also in parallel to each other. And after that - worker5.

In the logs, it will look like this:

2023-04-18 21:41:31 20434-20464 Worker1  D  doWork: start
2023-04-18 21:41:31 20434-20465 Worker2  D  doWork: start
2023-04-18 21:41:32 20434-20464 Worker1  D  doWork: end
2023-04-18 21:41:32 20434-20465 Worker2  D  doWork: end
2023-04-18 21:41:32 20434-20476 Worker3  D  doWork: start
2023-04-18 21:41:32 20434-20464 Worker4  D  doWork: start
2023-04-18 21:41:33 20434-20476 Worker3  D  doWork: end
2023-04-18 21:41:33 20434-20464 Worker4  D  doWork: end
2023-04-18 21:41:33 20434-20465 Worker5  D  doWork: start
2023-04-18 21:41:34 20434-20465 Worker5  D  doWork: end

The first and second tasks start at the same time. When they are both completed, the third and fourth start, also at the same time. When they are both completed, the fifth task starts.

Let's consider another case. Suppose we need the second task to be completed after the first, and the fourth after the third. We have two sequences and they can be run in parallel. And when these two sequences are completed, you need to start the fifth task.

This is how it is done:

val chain12: WorkContinuation = WorkManager  
    .getInstance(this)  
    .beginWith(worker1)  
    .then(worker2)  

val chain34: WorkContinuation = WorkManager  
    .getInstance(this)  
    .beginWith(worker3)  
    .then(worker4)  

WorkContinuation.combine(listOf(chain12, chain34))  
    .then(worker5)  
    .enqueue()

WorkContinuation is a sequence of tasks. We create a chain12 sequence consisting of the first and second tasks, and a chain34 sequence consisting of the third and fourth tasks. To make these sequences run in parallel to each other, we pass them to the combine method. Then we pass the fifth task to the then method, which starts after all the sequences from combine are completed.

Result:

2023-04-19 15:58:20  1511-1534  Worker1 D  doWork: start
2023-04-19 15:58:20  1511-1535  Worker3 D  doWork: start
2023-04-19 15:58:21  1511-1534  Worker1 D  doWork: end
2023-04-19 15:58:21  1511-1535  Worker3 D  doWork: end
2023-04-19 15:58:21  1511-1571  Worker2 D  doWork: start
2023-04-19 15:58:21  1511-1534  Worker4 D  doWork: start
2023-04-19 15:58:22  1511-1571  Worker2 D  doWork: end
2023-04-19 15:58:22  1511-1534  Worker4 D  doWork: end
2023-04-19 15:58:22  1511-1571  Worker5 D  doWork: start
2023-04-19 15:58:23  1511-1571  Worker5 D  doWork: end

The first and third tasks start, i.e. sequences start running in parallel. When both sequences are completed, the fifth task starts.

Unique work

We can make the task sequence unique. To do this, we start the sequence using the beginUniqueWork method.

private fun work() {  

    val worker1 = OneTimeWorkRequestBuilder<Worker1>()  
        .build()   

    val worker3 = OneTimeWorkRequestBuilder<Worker3>()  
        .build()  

    val worker5 = OneTimeWorkRequestBuilder<Worker5>()  
        .build()  

    WorkManager.getInstance(this)  
        .beginUniqueWork("work123", ExistingWorkPolicy.REPLACE, worker1)  
        .then(worker3)  
        .then(worker5)  
        .enqueue()  

}

findViewById<AppCompatButton>(R.id.startWork).setOnClickListener {  
    Log.e(TAG, "enqueue REPLACE")  
    work()  
}

Specify the name of the sequence, the mode, and the first task of the sequence.

We specified REPLACE as the mode. This means that if a sequence with the same name is already running, then another run will cause the currently running sequence to be stopped and a new one started.

I added logging to the call to the enqueue method, which starts the sequence. Let's see what's happening in the logs.

2023-04-19 16:52:32 17809-17809 WorkManagerActivity3 E  enqueue REPLACE
2023-04-19 16:52:33 17809-17882 Worker1              D  doWork: start
2023-04-19 16:52:38 17809-17882 Worker1              D  doWork: end
2023-04-19 16:52:38 17809-17914 Worker3              D  doWork: start
2023-04-19 16:52:43 17809-17914 Worker3              D  doWork: end
2023-04-19 16:52:43 17809-17952 Worker5              D  doWork: start

2023-04-19 16:52:46 17809-17809 WorkManagerActivity3 E  enqueue REPLACE
2023-04-19 16:52:46 17809-17880 Worker5              D  onStopped
2023-04-19 16:52:46 17809-17882 Worker1              D  doWork: start
2023-04-19 16:52:48 17809-17952 Worker5              D  doWork: end
2023-04-19 16:52:51 17809-17882 Worker1              D  doWork: end
2023-04-19 16:52:51 17809-17914 Worker3              D  doWork: start
2023-04-19 16:52:56 17809-17914 Worker3              D  doWork: end
2023-04-19 16:52:56 17809-17952 Worker5              D  doWork: start
2023-04-19 16:53:01 17809-17952 Worker5              D  doWork: end

16:52:32 is the first run of the sequence. Tasks start running one after another.

16:52:46 - while MyWorker5 is running, I create and start the same sequence with the same name - work123. The currently running sequence stops and a new one starts.

The KEEP mode will keep the currently executing sequence running. The new one will be ignored.

Code:

WorkManager.getInstance(this)  
    .beginUniqueWork("work123", ExistingWorkPolicy.KEEP, worker1)  
    .then(worker3)  
    .then(worker5)  
    .enqueue()

Logs:

2023-04-19 17:05:17 20030-20030 WorkManagerActivity3 E  enqueue KEEP
2023-04-19 17:05:18 20030-20207 Worker1              D  doWork: start
2023-04-19 17:05:23 20030-20207 Worker1              D  doWork: end
2023-04-19 17:05:23 20030-20227 Worker3              D  doWork: start
2023-04-19 17:05:28 20030-20227 Worker3              D  doWork: end
2023-04-19 17:05:28 20030-20272 Worker5              D  doWork: start
2023-04-19 17:05:29 20030-20030 WorkManagerActivity3 E  enqueue KEEP
2023-04-19 17:05:33 20030-20272 Worker5              D  doWork: end

17:05:29 - I tried to run the sequence again, but was ignored because there is already a sequence with the same name in the works.

APPEND mode will start a new sequence after executing the current one.

Code:

WorkManager.getInstance(this)  
    .beginUniqueWork("work123", ExistingWorkPolicy.APPEND, worker1)  
    .then(worker3)  
    .then(worker5)  
    .enqueue()

Logs:

2023-04-19 17:10:28 21636-21636 WorkManagerActivity3 E  enqueue APPEND
2023-04-19 17:10:28 21636-21730 Worker1              D  doWork: start
2023-04-19 17:10:33 21636-21730 Worker1              D  doWork: end
2023-04-19 17:10:33 21636-21784 Worker3              D  doWork: start
2023-04-19 17:10:38 21636-21784 Worker3              D  doWork: end
2023-04-19 17:10:38 21636-21836 Worker5              D  doWork: start
2023-04-19 17:10:39 21636-21636 WorkManagerActivity3 E  enqueue APPEND
2023-04-19 17:10:43 21636-21836 Worker5              D  doWork: end
2023-04-19 17:10:43 21636-21730 Worker1              D  doWork: start
2023-04-19 17:10:48 21636-21730 Worker1              D  doWork: end
2023-04-19 17:10:48 21636-21784 Worker3              D  doWork: start
2023-04-19 17:10:53 21636-21784 Worker3              D  doWork: end
2023-04-19 17:10:53 21636-21836 Worker5              D  doWork: start
2023-04-19 17:10:58 21636-21836 Worker5              D  doWork: end

17:10:39 - the current sequence was not interrupted, and a new one was launched immediately after the end of the current one.

Be careful with this mode, because. an error in the current sequence may result in the new sequence not starting.

I created and restarted the same sequence in these last examples, but the current and new sequences may consist of different tasks. The main thing here is the same name as the sequences.


Written by azamatnurkhojayev | I'm an Android developer. Teaches courses at the programming school, and writes articles about development.
Published by HackerNoon on 2023/05/03