В этом уроке рассмотрим как передать данные в задачу и как получить результат
Полный список уроков курса:
- Урок 1. Lifecycle
- Урок 2. LiveData
- Урок 3. LiveData. Дополнительные возможности
- Урок 4. ViewModel
- Урок 5. Room. Основы
- Урок 6. Room. Entity
- Урок 7. Room. Insert, Update, Delete, Transaction
- Урок 8. Room. Query
- Урок 9. Room. RxJava
- Урок 10. Room. Запрос из нескольких таблиц. Relation
- Урок 11. Room. Type converter
- Урок 12. Room. Миграция версий базы данных
- Урок 13. Room. Тестирование
- Урок 14. Paging Library. Основы
- Урок 15. Paging Library. PagedList и DataSource. Placeholders.
- Урок 16. Paging Library. LivePagedListBuilder. BoundaryCallback.
- Урок 17. Paging Library. Виды DataSource
- Урок 18. Android Data Binding. Основы
- Урок 19. Android Data Binding. Код в layout. Доступ к View
- Урок 20. Android Data Binding. Обработка событий
- Урок 21. Android Data Binding. Observable поля. Двусторонний биндинг.
- Урок 22. Android Data Binding. Adapter. Conversion.
- Урок 23. Android Data Binding. Использование с include, ViewStub и RecyclerView.
- Урок 24. Navigation Architecture Component. Введение
- Урок 25. Navigation. Передача данных. Type-safe аргументы.
- Урок 26. Navigation. Параметры навигации
- Урок 27. Navigation. NavigationUI.
- Урок 28. Navigation. Вложенный граф. Global Action. Deep Link.
- Урок 29. WorkManager. Введение
- Урок 30. WorkManager. Критерии запуска задачи.
- Урок 31. WorkManager. Последовательность выполнения задач.
- Урок 32. WorkManager. Передача и получение данных
- Урок 33. Практика. О чем это будет.
- Урок 34. Практика. TodoApp. Список задач.
- Урок 35. Практика. TodoApp. Просмотр задачи
Когда мы запускаем задачу, нам может понадобиться передать в нее данные и получить обратно результат. Давайте посмотрим, как это можно сделать.
Входные данные
Сначала рассмотрим как передать в задачу входные данные:
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();
После первой и второй задач запускаем параллельно третью и четвертую. В этом случае и в третью и в четвертую задачу придут входные данные, полученные из первой и второй задач.
Присоединяйтесь к нам в Telegram:
- в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.
- в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance
- ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня