WorkManager. Вступ
Важлива частина роботи програми - це фонова робота. Це може бути завантаження або аплоад, стиснення або розпакування, синхронізація тощо. Колись давно для фонової роботи були призначені сервіси. Але в Android 8 їх дуже сильно обмежили: якщо застосунок не активний, то і сервіс буде зупинений через якийсь час. Та й ще задовго до Android 8 розробники почали використовувати такі інструменти як JobScheduler або Firebase JobDispatcher для запуску фонових завдань.
WorkManager - "новий інструмент". Він дає змогу запускати фонові завдання послідовно або паралельно, передавати в них дані, отримувати з них результат, відстежувати статус виконання і запускати тільки за дотримання заданих умов. При цьому він дуже простий у використанні.
Завдання
Давайте створимо і запустимо фонове завдання.
Додайте в dependencies
implementation "android.arch.work:work-runtime:2.9.1"
Створюємо клас, що наслідує клас Worker:
public class MyWorker extends Worker {
static final String TAG = "workmng";
@NonNull
@Override
public WorkerResult doWork() {
Log.d(TAG, "doWork: start");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "doWork: end");
return WorkerResult.SUCCESS;
}
}
У метод doWork нам пропонується помістити код, який буде виконано. Я тут просто ставлю паузу в 10 секунд і повертаю результат SUCCESS, що означає, що все пройшло успішно. Нам не треба морочитися з потоками, тому що код буде виконано не в UI потоці.
Завдання готове. Тепер нам потрібно MyWorker обернути в WorkRequest:
OneTimeWorkRequest myWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class).build();
WorkRequest дає нам змогу задати умови запуску та вхідні параметри до завдання. Поки що ми нічого не задаємо, а просто створюємо OneTimeWorkRequest, якому говоримо, що запускати треба буде завдання MyWorker.
OneTimeWorkRequest не дарма має таку назву. Це завдання буде виконано один раз. Є ще PeriodicWorkRequest, але про нього трохи пізніше.
Тепер можна запускати завдання:
WorkManager.getInstance().enqueue(myWorkRequest);
Беремо WorkManager і в його метод enqueue передаємо WorkRequest. Після цього завдання буде запущено.
Дивимося лог:
20:37:36.567 5369-5444 doWork: start
20:37:46.568 5369-5444 doWork: end
Видно, що завдання виконувалося 10 секунд, і код виконувався не в UI потоці.
Статус завдання
WorkManager надає можливість відстежувати статус виконання завдання. Наприклад, в Activity пишемо:
WorkManager.getInstance().getStatusById(myWorkRequest.getId()).observe(this, new Observer<WorkStatus>() {
@Override
public void onChanged(@Nullable WorkStatus workStatus) {
Log.d(TAG, "onChanged: " + workStatus.getState());
}
});
У метод getStatusById необхідно передати ID завдання, який може бути отриманий методом WorkRequest.getId. У результаті ми отримуємо LiveData, підписуємося на нього і в метод onChanged нам будуть приходити всі зміни статусу нашого завдання. Методом WorkStatus.getState будемо отримувати поточний стан.
Запускаємо
20:52:54.189 6060-6060 onChanged: ENQUEUED
20:52:54.199 6060-6087 doWork: start
20:52:54.203 6060-6060 onChanged: RUNNING
20:53:04.200 6060-6087 doWork: end
20:53:04.211 6060-6060 onChanged: SUCCEEDED
Відразу після виклику методу enqueue завдання перебуває в статусі ENQUEUED. Потім WorkManager визначає, що завдання можна запускати і виконує наш код. У цей момент статус змінюється на RUNNING. Після виконання статус буде SUCCEEDED, тому що ми повернули такий статус у методі doWork.
Статус нам приходить в UI потоці.
Тепер ще раз запустимо завдання і закриємо додаток:
20:58:19.402 doWork: start
20:58:19.424 onChanged: ENQUEUED
20:58:19.462 onChanged: RUNNING
20:58:29.403 doWork: end
Зверніть увагу, завдання завершилося, а статус SUCCEEDED не прийшов. Чому? Тому що, закривши Activity, ми всього лише відписалися від LiveData, який передавав нам статуси завдання. Але саме завдання нікуди не поділося. Воно ніяк не залежить від застосунку і буде виконуватися, навіть якщо застосунок закрито.
Результат
Ми в нашому завданні повертали статус WorkerResult.SUCCESS, тим самим повідомляючи, що все ок. Є ще два варіанти:
FAILURE- у цьому випадку після завершення завданняworkStatus.getStateповернеFAILED. Для нас це сигнал, що завдання не було виконано.RETRY- а цей результат є сигналом для WorkManager, що завдання треба повторити. У цьому випадкуworkStatus.getStateповерне нам статусENQUEUED- тобто завдання знову заплановане.
Я протестував на емуляторі поведінку під час RETRY: перший раз завдання було перезапущено приблизно через одну хвилину після попереднього завершення. З кожним наступним перезапуском інтервал збільшувався:
21:10:22.637 doWork: start
21:10:32.638 doWork: end
21:11:32.655 doWork: start
21:11:42.657 doWork: end
21:14:07.538 doWork: start
21:14:17.543 doWork: end
21:18:17.561 doWork: start
21:18:27.602 doWork: end
21:26:27.618 doWork: start
21:26:37.653 doWork: end
Скасування завдання
Ми можемо скасувати завдання методом cancelWorkById, передавши ID завдання
WorkManager.getInstance().cancelWorkById(myWorkRequest.getId());
При цьому в класі MyWorker буде викликано метод onStopped (якщо ви його реалізували). Також у класі MyWorker ми завжди можемо використовувати boolean метод isStopped для перевірки того, що завдання було скасовано.
Якщо відстежуємо статус завдання, то WorkStatus.getState поверне Cancelled.
Також є метод cancelAllWork, який скасує всі ваші завдання. Але хелп попереджає, що він вкрай небажаний до використання, тому що може зачепити роботу бібліотек, які ви використовуєте.
Tag
Завданню можна присвоїти тег методом addTag:
OneTimeWorkRequest myWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
.addTag("mytag")
.build();
Одному завданню можна додавати кілька тегів.
У WorkStatus є метод getTags, який поверне всі теги, які присвоєні цьому завданню.
Присвоївши один тег кільком завданням, ми можемо всіх їх скасувати методом cancelAllWorkByTag:
WorkManager.getInstance().cancelAllWorkByTag("mytag");
setInitialDelay
Виконання завдання можна відкласти на зазначений час
OneTimeWorkRequest myWorkRequest = new OneTimeWorkRequest.Builder(MyWorker.class)
.setInitialDelay(10, TimeUnit.SECONDS)
.build();
У методі setInitialDelay ми вказали, що завдання слід запустити через 10 секунд після передачі його в WorkManager.enqueue
Періодичне завдання
Розглянутий нами OneTimeWorkRequest - це разове завдання. А якщо потрібно багаторазове виконання через певний період часу, то можна використовувати PeriodicWorkRequest:
PeriodicWorkRequest myWorkRequest = new PeriodicWorkRequest.Builder(MyWorker.class, 30, TimeUnit.MINUTES)
.build();
У білдері задаємо інтервал у 30 хвилин. Тепер завдання буде виконуватися з цим інтервалом.
Мінімально доступний інтервал - 15 хвилин. Якщо поставите менше, WorkManager сам підвищить до 15 хвилин.
WorkManager гарантує, що завдання буде запущено один раз протягом зазначеного інтервалу. І це може статися в будь-який момент інтервалу - через 1 хвилину, через 10 або через 29.
За допомогою параметра flex можна обмежити дозволений діапазон часу запуску.
PeriodicWorkRequest myWorkRequest = new PeriodicWorkRequest.Builder(MyWorker.class, 30, TimeUnit.MINUTES, 25, TimeUnit.MINUTES)
.build();
Крім інтервалу в 30 хвилин додатково передаємо в білдер flex параметр 25 хвилин. Тепер завдання буде запущено не в будь-який момент 30-хвилинного інтервалу, а тільки після 25-ї хвилини. Тобто між 25 і 30 хвилинами.
Context
Щоб отримати Context у класі Worker, використовуйте метод getApplicationContext.
Перезавантаження
Що відбувається із запланованими завданнями під час перезавантаження пристрою? Я протестував цей кейс на емуляторі і з'ясував, що всі завдання зберігаються. Тобто OneTimeWorkRequest з відкладеним запуском, OneTimeWorkRequest з результатом RETRY, PeriodicWorkRequest - всі ці завдання будуть знову запущені після перезавантаження пристрою.
Тому дійте обдумано і зберігайте десь у себе ID або тег завдання, щоб ви могли його скасувати, якщо воно вам більше не потрібне.