WorkManager отлично подходит для планирования фоновой работы на Android. Однако, поскольку такая отложенная работа находится за пределами жизненного цикла приложения, вы можете столкнуться с неожиданными сбоями.
Читайте дальше, чтобы узнать, почему и как предотвратить это.
Как сломать WorkManager
Читая документацию, становится ясно, что WorkManager — это простое решение для фоновой работы:
WorkManager — рекомендуемое решение для непрерывной работы. Его функционирование является постоянным даже после перезапуска приложения или перезагрузки системы.
Это очень здорово!
Итак, если мы запланируем какую-то работу, например, загрузку данных креша:
val workerClass = CrashUploadWorker::class.java
WorkManager.getInstance(application)
.enqueue(OneTimeWorkRequest.Builder(workerClass).build())
Мы можем быть уверены, что WorkManager справится с этим за нас, даже если приложение само закроется сразу после сбоя.
Однако WorkManager предполагает, что класс Worker всегда будет существовать в нашем приложении. Итак, если мы выпустим новую версию нашего приложения, которая либо:
- удаляет CrashUploadWorker
- переименовывает CrashUploadWorker в CrashReportWorker
- перемещает CrashUploadWorker в новый пакет
мы можем получить сбой ClassNotFoundException после установки обновления!
java.lang.Error: java.lang.ClassNotFoundException: com.example.CrashUploadWorker
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1119)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
at java.lang.Thread.run(Thread.java:818)
Происходит это потому, что WorkManager живет в отдельном процессе (Google Play Services) и всегда будет пытаться завершить свою работу. В данном случае он попытается создать экземпляр CrashUploadWorker, но его больше нет в нашем приложении.
К сожалению, мне пришлось понять этот на практике.
Обратите внимание на использование слова «может»: сбой не гарантируется и произойдет только в том случае, если во время обновления приложения была незавершенная работа.
Как не сломать WorkManager
Первое, что вы можете попробовать, это отменить всю незавершенную работу для рабочего, которого вы удалили/переименовали:
workManager.cancelAllWorkByTag("crash_upload")
Этот подход может зависеть от условий гонки, так как Workmanager все равно может повторить попытку выполнить запланированную работу до того, как у вас появится возможность ее отменить (в зависимости от того, где вы это вызываете).
Другим недостатком этого подхода является то, что запланированная работа будет прервана, что приведет к потере данных. В зависимости от вашего уникального варианта использования это может быть или не быть приемлемым.
Альтернативный подход состоит в том, чтобы сохранить исходный класс CrashUploadWorker и изменить его для обработки изменившихся требований:
- отменить работу (пустая реализация)
- миграция и планирование нового класса воркера
internal class CrashUploadWorker(
appContext: Context,
workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
override suspend fun doWork(): Result {
// Schedule new worker class
val workerClass = CrashReportWorker::class.java
WorkManager.getInstance(appContext)
.enqueue(OneTimeWorkRequest.Builder(workerClass).build())
return Result.success()
}
}
Как только вы перестанете планировать работу с помощью старого Worker, вы можете пометить его как удаленный после того, как все ваши клиенты обновятся и мигрируют.
Вот как может выглядеть план миграции:
- Версия 1: добавление нового воркера и перенос всей работы.
- Версия 5: отмена всей оставшейся работы с использованием старого Worker (приводит к потере данных!)
- Релиз 10: удаление старого Worker (вызывает сбои!)
Использование WorkerFactory
Альтернативный подход — предоставить пользовательский WorkerFactory для обработки миграции в новый класс.
Для этого сначала отключите автоматическую инициализацию WorkManager:
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
Затем инициализируйте WorkManager в вашем Application#onCreate или ContentProvider:
val configuration = Configuration.Builder()
.setWorkerFactory(MigrateWorkerFactory())
.build()
WorkManager.initialize(appContext, configuration)
И создайте свой собственный WorkerFactory, который запланирует новый воркер:
class MigrateWorkerFactory() : WorkerFactory() {
override fun createWorker(
appContext: Context,
workerClassName: String,
workerParameters: WorkerParameters
): ListenableWorker? {
if (workerClassName = "com.example.CrashUploadWorker") {
return CrashReportWorker(appContext, workerParameters)
}
...
}
}
Преимущество этого заключается в том, что нет необходимости сохранять старый класс Worker, но возникает дополнительная сложность ручной инициализации WorkManager.
Итого
WorkManager — очень удобный инструмент для фоновой работы, но будьте осторожны при удалении или переименовании воркеров.

