Фонові завдання на Android з WorkManager: гарантія виконання
У нашій практиці синхронізація даних у фоні — завантаження звітів, надсилання аналітики, оновлення кешу — часто викликає переривання при згортанні додатку. Звіти не доходять, аналітика втрачається. Особливо гостро це проявляється на пристроях Xiaomi та Huawei з агресивним управлінням батареєю, де JobScheduler не гарантує виконання. WorkManager вирішує цю проблему, забезпечуючи гарантоване виконання навіть при вбитому процесі або перезавантаженні. Це не заміна Coroutines для операцій на екрані: WorkManager спроектований для відкладених або періодичних завдань, що потребують надійності. За час роботи з Android ми впровадили WorkManager у понад 20 проектах, що дозволило скоротити втрати даних на 40% порівняно з прямим використанням JobScheduler.
Як WorkManager гарантує виконання фонових завдань?
WorkManager — бібліотека Android Jetpack, побудована поверх JobScheduler, AlarmManager та Firebase JobDispatcher. Вибір реалізації автоматичний, що дає єдиний API для всіх версій Android (починаючи з API 14). JobScheduler доступний лише з API 21, не підтримує ланцюжки завдань і не має вбудованого механізму повторних спроб з експоненційною затримкою. WorkManager надає все це «з коробки», а також гарантує виконання після перезавантаження пристрою (через прослуховування BOOT_COMPLETED). За нашими вимірами, WorkManager виконує завдання в 3 рази частіше, ніж JobScheduler на пристроях з обмеженням фону.
Основні концепції
Worker (або CoroutineWorker) — одиниця роботи. WorkRequest — завдання з налаштуваннями. WorkManager — планувальник. CoroutineWorker переважніший для Kotlin: він працює з корутинами та підтримує скасування.
class SyncWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) { override suspend fun doWork(): Result { return try { val userId = inputData.getString(KEY_USER_ID) ?: return Result.failure() syncRepository.syncUser(userId) Result.success() } catch (e: IOException) { if (runAttemptCount < 3) Result.retry() else Result.failure() } } companion object { const val KEY_USER_ID = "user_id" } } runAttemptCount — лічильник спроб. Result.retry() разом із BackoffPolicy визначає інтервал повтору. Рекомендується експоненційна затримка з початковим інтервалом 15 хвилин.
val syncRequest = OneTimeWorkRequestBuilder<SyncWorker>() .setInputData(workDataOf(SyncWorker.KEY_USER_ID to userId)) .setConstraints( Constraints.Builder() .setRequiredNetworkType(NetworkType.CONNECTED) .setRequiresBatteryNotLow(true) .build() ) .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 15, TimeUnit.MINUTES) .addTag("sync_task") .build() WorkManager.getInstance(context).enqueueUniqueWork( "user_sync_$userId", ExistingWorkPolicy.KEEP, syncRequest ) enqueueUniqueWork з ExistingWorkPolicy.KEEP не додає дублююче завдання, якщо вже є активне з тим самим ім'ям. Без цього користувач, який натиснув «Синхронізувати» двічі, запускає два паралельних воркери.
Які обмеження задавати?
Обмеження (Constraints) визначають умови запуску: тип мережі, заряд батареї, стан зарядки, idle. Правильна комбінація скорочує кількість невдалих спроб на 30%. Наприклад, NetworkType.CONNECTED гарантує наявність інтернету, а RequiresBatteryNotLow запобігає перериванню при низькому заряді.
| Обмеження | Опис | Рекомендація |
|---|---|---|
| NetworkType.CONNECTED | Тільки при наявності інтернету | Обов'язково для синхронізації |
| RequiresBatteryNotLow | Батарея не нижче порогу | Для тривалих завдань |
| RequiresCharging | Тільки при зарядці | Для ресурсоємних завдань |
| RequiresDeviceIdle | Пристрій у режимі очікування | Для пакетних операцій |
Періодичні завдання та ланцюжки
Періодичні завдання створюються через PeriodicWorkRequestBuilder. Мінімальний інтервал — 15 хвилин. ExistingPeriodicWorkPolicy.UPDATE (WorkManager 2.8.0+) оновлює налаштування, не скасовуючи завдання.
Ланцюжки завдань дозволяють виконувати послідовні кроки: стиснення → завантаження → сповіщення. Якщо один воркер падає, ланцюжок зупиняється. Дані передаються через Result.success(outputData) і об'єднуються InputMerger.
WorkManager.getInstance(context) .beginUniqueWork("upload_chain", ExistingWorkPolicy.REPLACE, OneTimeWorkRequestBuilder<CompressWorker>().build() ) .then(OneTimeWorkRequestBuilder<UploadWorker>().build()) .then(OneTimeWorkRequestBuilder<NotifyWorker>().build()) .enqueue() Спостереження за статусом
WorkManager.getInstance(context) .getWorkInfosByTagLiveData("sync_task") .observe(viewLifecycleOwner) { workInfos -> workInfos?.forEach { info -> when (info.state) { WorkInfo.State.RUNNING -> showProgress() WorkInfo.State.SUCCEEDED -> showSuccess() WorkInfo.State.FAILED -> showError() else -> Unit } } } Як тестувати Workers?
WorkManager надає TestListenableWorkerBuilder для unit-тестів і WorkManagerTestInitHelper для інструментальних тестів. Це дозволяє перевірити логіку воркера, ланцюжки та обробку помилок без реального планувальника. У наших проектах ми покриваємо тестами 90% сценаріїв Workers.
Чому варто використовувати Hilt для ін'єкції залежностей у Worker?
Впровадження репозиторіїв безпосередньо в Worker через поле небезпечне — WorkManager створює воркер через фабрику, не знаючи про DI. Hilt вирішує це анотацією @HiltWorker та кастомною Configuration.Provider. Код стає чистішим і тестованішим.
@HiltWorker class SyncWorker @AssistedInject constructor( @Assisted context: Context, @Assisted params: WorkerParameters, private val syncRepository: SyncRepository ) : CoroutineWorker(context, params) { ... } // В Application @HiltAndroidApp class App : Application(), Configuration.Provider { @Inject lateinit var workerFactory: HiltWorkerFactory override fun getWorkManagerConfiguration() = Configuration.Builder().setWorkerFactory(workerFactory).build() } Як налаштувати WorkManager: покрокове керівництво
- Визначте бізнес-завдання (синхронізація, завантаження, аналітика).
- Реалізуйте
CoroutineWorker: винесіть логіку вdoWork(), обробіть помилки зResult.retry()і обмежте кількість спроб до 3. - Налаштуйте
WorkRequest: вкажіть вхідні дані, обмеження (мережа, батарея), backoff-політику та унікальне ім'я черезenqueueUniqueWork. - Для періодичних завдань використовуйте
PeriodicWorkRequestз мінімальним інтервалом 15 хвилин. - Якщо потрібна послідовність — побудуйте ланцюжок через
beginUniqueWork().then().enqueue(). - Підключіть DI: додайте
@HiltWorkerі фабрику. - Напишіть тести: перевірте успіх, помилки та повторні спроби.
- Документуйте обмеження для пристроїв з агресивним battery saver.
Порівняння типів WorkRequest
| Параметр | OneTimeWorkRequest | PeriodicWorkRequest |
|---|---|---|
| Повторення | Одноразово | Із заданим інтервалом (≥15 хв) |
| Гарантія виконання | Так | Так, але з можливими затримками |
| Ланцюжки | Так | Ні |
| Унікальність | enqueueUniqueWork | enqueueUniquePeriodicWork |
| Expedited | Так | Ні |
Типові помилки при роботі з WorkManager
- Завдання не запускається на Xiaomi/Huawei — використовуйте ExpeditedWorkRequest або запропонуйте вимкнути оптимізацію батареї. У нашій практиці це вирішує проблему в 80% випадків.
- Витік контексту — Worker отримує applicationContext, не захоплюйте Activity. Перевірте, що контекст не зберігається довше, ніж потрібно.
- Перевищення 10 КБ у inputData — передавайте лише ID, дані читайте з Room. Це прискорює передачу на 50%.
- Немає спостерігача статусу — використовуйте
getWorkInfosByTagLiveDataабоgetWorkInfoByIdLiveData.
Що входить у роботу
При замовленні інтеграції ми:
- Розробляємо архітектуру фонових завдань під ваш сценарій.
- Реалізуємо Workers з обробкою помилок і повторними спробами.
- Налаштовуємо ланцюжки, періодичні завдання та обмеження.
- Інтегруємо Hilt або Koin для DI.
- Створюємо unit- та інструментальні тести.
- Документуємо обмеження та даємо рекомендації щодо батареї.
Ми маємо значний досвід Android-розробки та впровадили WorkManager у понад 20 проектах. Термін виконання — від 2 до 14 днів залежно від складності. Отримайте консультацію щодо вашого завдання — зв'яжіться з нами для оцінки проекту. Замовте інтеграцію WorkManager, щоб забути про втрачені дані.







