WorkManager. Передавання та отримання даних

Коли ми запускаємо завдання, нам може знадобитися передати в нього дані й отримати назад результат. Давайте подивимося, як це можна зробити.

Вхідні дані

Спочатку розглянемо як передати в задачу вхідні дані:

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

OneTimeWorkRequest myWorkRequest1 = new OneTimeWorkRequest.Builder(MyWorker1.class)
       .setInputData(myData)
       .build();

Дані поміщаємо в об'єкт Data за допомогою його білдера. Далі цей об'єкт передаємо в метод setInputData білдера WorkRequest.

Коли завдання буде запущено, то всередині нього (у MyWorker1.java) ми можемо отримати ці вхідні дані так:

String valueA = getInputData().getString("keyA", "");
int valueB = getInputData().getInt("keyB", 0);

Вихідні дані

Щоб завдання повернуло дані, необхідно передати їх у метод setOutputData. Код у MyWorker1.java буде таким:

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

Ці вихідні дані ми зможемо дістати з WorkStatus

workStatus.getOutputData().getString("keyC", "")

У об'єкта Data, який зберігає дані, є метод getKeyValueMap, який поверне вам immutable Map, що містить усі дані цього Data.

А у Data.Builder є метод putAll(Map<String, Object> values), в який ви можете передати Map, всі дані з якого будуть поміщені в Data.

Дані між завданнями

Якщо ви створюєте послідовність завдань, то вихідні дані попереднього завдання передаватимуться як вхідні в наступне завдання.

Наприклад, запускаємо послідовність із першого та другого завдань

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

Якщо перша задача повертає такі вихідні дані:

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

То в другій вони прийдуть, як вхідні і ми можемо отримати їх звичайним шляхом

String valueA = getInputData().getString("keyA", "");
int valueB = getInputData().getInt("keyB", 0);

Трохи ускладнимо приклад:

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

Перше і друге завдання виконуються паралельно, потім виконується третє. У результаті вихідні дані з першої та другої задач потраплять у третю. Давайте подивимося, як це вийде.

Нехай перше завдання повертає такі дані:

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

А друга - такі

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

Зверніть увагу, я спеціально зробив однакові ключі: keyA і keyB, щоб перевірити, які значення цих ключів надійдуть до третьої задачі - з першої задачі чи з другої.

Виводжу в лог вхідні дані третього завдання:

Log.d(TAG, "work3, data " + getInputData().getKeyValueMap());

Результат:

work3, data {keyA=value2, keyB=2, keyC=valueC, keyD=valueD}

В однакових ключах (keyA і keyB) ми бачимо, що прийшли дані з другого завдання. Спочатку я вирішив, що так сталося, тому що друге завдання виконується трохи довше за перше, і логічно, що його значення просто перезатерли значення з першого завдання під час збігу ключів. Але потім я знову запустив цю послідовність і отримав такий результат.

work3, data {keyA=value1, keyB=1, keyC=valueC, keyD=valueD}

Тепер ми бачимо значення першого завдання в ключах keyA і keyB.

Тобто якщо завдання виконуються паралельно, то в разі збігу ключів невідомо, з якого саме завдання ви отримаєте значення. Тому тут будьте акуратніше.

InputMerger Щоб перетворити кілька вихідних результатів в один вхідний, використовується InputMerger. Існує кілька його реалізацій, за замовчуванням використовується OverwritingInputMerger. Ми вже подивилися, як він працює. Якщо ключ збігається, то залишиться тільки одне значення.

Розглянемо ще один InputMerger - ArrayCreatingInputMerger. Він у разі збігу ключів створить масив, у який помістить усі значення цього ключа.

Давайте для третього завдання вкажемо його методом setInputMerger:

OneTimeWorkRequest myWorkRequest3 = new OneTimeWorkRequest.Builder(MyWorker3.class)
       .setInputMerger(ArrayCreatingInputMerger.class)
       .build();

Тепер під час злиття вихідних даних із попередніх завдань у вхідні дані третього завдання буде використовуватися ArrayCreatingInputMerger.

Результат його роботи - це завжди масив, навіть якщо не було збігів ключів

String[] valueA = getInputData().getStringArray("keyA");
int[] valueB = getInputData().getIntArray("keyB");
String[] valueC = getInputData().getStringArray("keyC");
String[] valueD = getInputData().getStringArray("keyD");

Для перевірки використовуємо той самий приклад:

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

Перше і друге завдання виконуються паралельно і їхні вихідні дані формуватимуть вхідні дані для третього завдання

Перше завдання поверне такі дані:

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

а друга - такі

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

У третій ми отримаємо такі вхідні дані:

valueA = [value1, value2]
valueB = [1, 2]
valueC = [valueC]
valueD = [valueD]

Тепер у разі збігу ключів дані не перезатираються, а складаються в масив.

Custom merger

За необхідності ми можемо написати свій InputMerger. Для цього треба просто успадкувати клас InputMerger і реалізувати його метод:

Data merge(@NonNull List<Data> inputs)

На вхід ми отримуємо кілька вихідних даних із попередніх завдань, і від нас вимагається смержити їх у вхідні дані для наступного завдання.

Далі залишається тільки передати свій Merger у setInputMerger.

Ось приклад свого Merger:

public class MyMerger extends InputMerger {

    @Override
    public @NonNull
    Data merge(@NonNull List<Data> inputs) {
        Data.Builder output = new Data.Builder();
        Map<String, Object> mergedValues = new HashMap<>();

        for (Data input : inputs) {
            mergedValues.putAll(input.getKeyValueMap());
        }

        output.putAll(mergedValues);
        output.putInt("input_data_count", inputs.size() - 1);
        return output.build();
    }

}

Код майже повністю повторює OverwritingInputMerger.

Створюємо Map і в циклі складаємо в нього дані з усіх Data, що надійшли. У разі збігу ключів, значення будуть перезаписані. Далі цей Map передаємо в білдер output. І від себе додаємо ключ input_data_count, у який поміщаємо кількість об'єктів Data, що прийшли нам. Тим самим вхідні дані наступного завдання міститимуть кількість паралельно виконаних попередніх завдань.

Мінус 1 потрібен тому, що список, який йде на вхід методу merge, містить Data, не тільки ті, що надійшли з попередніх завдань, а й Data, задані в білдері WorkRequest (метод setInputData) завдання, яке використовує цей Merger. Навіть якщо ми явно його не задавали, він існує і прийде порожній.

Готуємо список паралельних завдань:

List<OneTimeWorkRequest> workRequests = new LinkedList<>();
workRequests.add(myWorkRequest1);
workRequests.add(myWorkRequest2);
workRequests.add(myWorkRequest3);

Задачі, яка отримуватиме результати, задаємо MyMerger:

OneTimeWorkRequest myWorkRequest4 = new OneTimeWorkRequest.Builder(MyWorker4.class)
        .setInputMerger(MyMerger.class)
        .build();

Запускаємо це все:

WorkManager.getInstance()
        .beginWith(workRequests)
        .then(myWorkRequest4)
        .enqueue();

І в MyWorker4 отримуємо змерзлі дані та count=3.

Ну і наостанок опишу ще один випадок:

WorkManager.getInstance()
       .beginWith(myWorkRequest1, myWorkRequest2)
       .then(myWorkRequest3, myWorkRequest4)
       .enqueue();

Після першої та другої задач запускаємо паралельно третю і четверту. У цьому випадку і в третю, і в четверту задачу надійдуть вхідні дані, отримані з першої та другої задач.