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();
Після першої та другої задач запускаємо паралельно третю і четверту. У цьому випадку і в третю, і в четверту задачу надійдуть вхідні дані, отримані з першої та другої задач.