Running Background Tasks in Android with WorkManager: Part 4

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

TLDRIn this article, we will look at how to pass data to a task and how to get a result. The data is placed in the `Data` object using its builder. Next, we pass this object to the `setInputData` method of the WorkRequest builder. For the task to return data, you must pass it to the ‘setOutputData’ method.via the TL;DR App

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

Sending and receiving data

In this article, we will look at how to pass data to a task and how to get a result.

When we run a task, we may need to pass data to it and get back the result. Let's see how this can be done.

Input data

First, let's look at how to pass input data to the task:

val myData: Data = Data.Builder()  
    .putString("keyA", "value1")  
    .putInt("keyB", 1)  
    .build()

val myWorkRequest: OneTimeWorkRequest = OneTimeWorkRequestBuilder<MyFirstWorker>()  
    .setInputData(myData)  
    .build()

  
WorkManager  
    .getInstance(this)  
    .enqueue(myWorkRequest)

The data is placed in the Data object using its builder. Next, we pass this object to the setInputData method of the WorkRequest builder.

When the task is running, then inside it (in MyFirstWorker) we can receive input like this:

class MyFirstWorker(context: Context, parameters: WorkerParameters): Worker(context, parameters) {  

    private val TAG = this.javaClass.simpleName  

    override fun doWork(): Result {  

        val valueA = inputData.getString("keyA")  
        val valueB = inputData.getInt("keyB", 0)  
        Log.d(TAG, "doWork:valueA $valueA")  
        Log.d(TAG, "doWork:valueB $valueB")  

        return Result.success()  

    }  

}

Logs:

2023-04-20 12:44:00 29725-29754 MyFirstWorker D  doWork:valueA value1
2023-04-20 12:44:00 29725-29754 MyFirstWorker D  doWork:valueB 1

Output data

For the task to return data, you must pass it to the setOutputData method. The code in MyFirstWorker would be:

    override fun doWork(): Result {   

        val output = Data.Builder()  
            .putString("keyC", "value11")  
            .putInt("keyD", 11)  
            .build()  

        return Result.success(output)  

    }

We can get this output from WorkInfo:

WorkManager  
    .getInstance(this)  
    .getWorkInfoByIdLiveData(myWorkRequest.id)  
    .observe(this) { info ->  
        when (info?.state) {  
            WorkInfo.State.FAILED -> {  

            }  

            WorkInfo.State.SUCCEEDED -> {  

                val valueC = info.outputData.getString("keyC")  
                val valueD = info.outputData.getInt("keyD", 0)  
                Log.e(TAG, "value: $valueC" )  
                Log.e(TAG, "value: $valueD" )  

            }  

            else -> {  

            }  

        }  

    }

Result:

2023-04-21 08:33:20 32749-32749 WorkManagerActivity E  value: value11
2023-04-21 08:33:20 32749-32749 WorkManagerActivity E  value: 11

The Data object that stores the data has a getKeyValueMap method that will return to you a Map containing all the information of this Data.

And Data.Builder has a putAll(Map<String, Object> values) method into which you can pass a Map, all the information which will be placed in Data.

Data between tasks

If you create a task sequence, the output of the previous task will be passed as input to the subsequent task.

For example, we run a sequence of the first and second tasks:

WorkManager  
    .getInstance(this)  
    .beginWith(myWorkRequest1)  
    .then(myWorkRequest2)  
    .enqueue()

If the first task returns output data like this:

    override fun doWork(): Result {    

        val output = Data.Builder()  
            .putString("keyA", "value1")  
            .putInt("keyB", 1)  
            .build()  

        return Result.success(output)  

    }

Then in the second, they will come as input and we can get them in the usual way:

override fun doWork(): Result {  

    val valueA = inputData.getString("keyA")  
    val valueB = inputData.getInt("keyB", 0)  
    Log.d(TAG, "doWork:valueA $valueA")  
    Log.d(TAG, "doWork:valueB $valueB")  

    return Result.success()  

}

Logs:

2023-04-21 08:53:24 628-661 MySecondWorker D  doWork:valueA value1
2023-04-21 08:53:24 628-661 MySecondWorker D  doWork:valueB 1

Let's complicate the example a bit:

WorkManager  
    .getInstance(this)  
    .beginWith(listOf(myWorkRequest1, myWorkRequest2))  
    .then(myWorkRequest3)  
    .enqueue()

The first and second tasks are executed in parallel, then the third one is executed. As a result, the output from the first and second tasks will fall into the third. Let's see how it turns out.

Let the first task return the following data:

    override fun doWork(): Result {  

        val output = Data.Builder()  
            .putString("keyA", "value1")  
            .putInt("keyB", 1)  
            .putString("keyC", "valueC")  
            .build()  

        return Result.success(output)  

    }

And the second one is:

    override fun doWork(): Result {  

        val output = Data.Builder()  
            .putString("keyA", "value2")  
            .putInt("keyB", 2)  
            .putString("keyD", "valueD")  
            .build()  

        return Result.success(output)  

    }

Please note that I specifically made the same keys: keyA and keyB in order to check which values of these keys will come to the third task - from the first task or from the second.

I output the input data of the third task to the log:

    override fun doWork(): Result {  

        Log.d(TAG, "data " + inputData.keyValueMap)  
        
        return Result.success()  

    }

Result:

2023-04-21 09:04:02 783-838 MyThirdWorker D data {keyA=value2, keyB=2, keyC=valueC, keyD=valueD}

In the same keys (keyA and keyB), we see that the data came from the second task. At first, I thought that this happened because the second task took a little longer than the first, and it is logical that its values would overwrite the values from the first task when the keys matched. But then I ran that sequence again and got this result.

2023-04-21 09:05:47 963-995 MyThirdWorker D data {keyA=value1, keyB=1, keyC=valueC, keyD=valueD}

Now we see the values of the first task in the keys keyA and keyB.

If the tasks are executed in parallel, and if the keys match, it is not known from which task you will get the value. So be careful here.

InputMerger

To convert multiple outputs into a single input, use InputMerger. There are several implementations for this, and the default is OverwritingInputMerger. We have already seen how it works. If the key matches, then only one value will remain.

Consider another InputMerger - ArrayCreatingInputMerger. If the keys match, it will create an array in which it will place all the values of this key.

Let's specify it for the third task using the setInputMerger method:

val myWorkRequest3: OneTimeWorkRequest = OneTimeWorkRequestBuilder<MyThirdWorker>()  
    .setInputMerger(ArrayCreatingInputMerger::class.java)  
    .build()

The ArrayCreatingInputMerger will now be used when merging the output from the previous tasks into the input of the third task.

The result of its work is always an array, even if there were no key matches:

    override fun doWork(): Result {  

        val valueA = inputData.getStringArray("keyA")  
        val valueB = inputData.getIntArray("keyB")  
        val valueC = inputData.getStringArray("keyC")  
        val valueD = inputData.getStringArray("keyD")  

        Log.d(TAG, "valueA ${valueA?.toList()}")  
        Log.d(TAG, "valueB ${valueB?.toList()}")  
        Log.d(TAG, "valueC ${valueC?.toList()}")  
        Log.d(TAG, "valueD ${valueD?.toList()}")  

        return Result.success()  

    }

Let's use the same example to test it:

WorkManager  
    .getInstance(this)  
    .beginWith(listOf(myWorkRequest1, myWorkRequest2))  
    .then(myWorkRequest3)  
    .enqueue()

The first task will return the following data:

val output = Data.Builder()  
    .putString("keyA", "value1")  
    .putInt("keyB", 1)  
    .putString("keyC", "valueC")  
    .build()

and the second one is:

val output = Data.Builder()  
    .putString("keyA", "value2")  
    .putInt("keyB", 2)  
    .putString("keyD", "valueD")  
    .build()

In the third, we will receive the following input data:

2023-04-21 10:22:55  2307-2340  MyThirdWorker D valueA [value2, value1]
2023-04-21 10:22:55  2307-2340  MyThirdWorker D valueB [2, 1]
2023-04-21 10:22:55  2307-2340  MyThirdWorker D valueC [valueC]
2023-04-21 10:22:55  2307-2340  MyThirdWorker D valueD [valueD]

Now, when the keys match, the data is not overwritten but added to an array.


Also published here.


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/10