С релизом Android 12 приложения, где новая версия операционки будет указана в targetSdkVersion, получат запрет на запуск foreground-сервисов в бэкграунде. В качестве альтернативы Google предлагает WorkManager, который с появлением expedited jobs станет предпочтительным вариантом для запуска высокоприоритетных фоновых задач.
О нём и пойдёт речь в статье — под катом обсудим новые возможности инструмента, подключим его к приложению и реализуем миграцию с foreground-сервиса.
WorkManager и foreground service
Для справки:
-
Foreground service — это какой-либо сервис, о котором знает пользователь через нотификацию в статус-баре. Например, воспроизведение музыки или работа GPS в картах.
-
WorkManager — это API для планирования задач, которые будут выполняться, даже если выйти из приложения или перезагрузить устройство.
WorkManager уже давно является приоритетным способом выполнения длительных фоновых задач. К таким относятся синхронизация данных с бэкендом, отправка аналитики, периодическая проверка свободного места в системе с помощью PeriodicWork и так далее.
Но в WorkManager присутствовал и недостаток — не было никаких гарантий, что джоба начнётся незамедлительно после создания. В версии 2.3.0 разработчики добавили для воркеров метод setForegroundAsync(), который, по сути, превращал фоновую задачу в foreground-сервис и позволял немедленно приступить к её выполнению.
Такой подход ничем особо не отличался от разработки foreground-сервиса вручную, когда необходимо создавать объекты Notification и NotificationChannel при таргете выше, чем на Android Nougat.
private fun createInfo(): ForegroundInfo {
return ForegroundInfo(getNotificationId(), createNotification())
}
Сейчас setForegroundAsync() объявлен устаревшим, а при попытке запустить сервис из бэкграунда на выходе будет ForegroundServiceStartNotAllowedException.
И тут на сцену выходят expedited jobs.
Expedited jobs
Этот тип джобов позволяет приложениям выполнять короткие и важные задачи, давая системе больше контроля над доступом к ресурсам. Он находится где-то между foreground-сервисами и привычными джобами WorkManager. От последних их отличает:
-
минимально отложенное время запуска;
-
обход ограничений Doze-mode на использование сети;
-
меньшая вероятность быть «убитыми» системой.
А ещё в них не поддерживаются ограничения по заряду батареи и режиму работы девайса:
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.NOT_ROAMING)
.setRequiresStorageNotLow(true)
/*
Неподдерживаемые ограничения
.setRequiresCharging(false)
.setRequiresDeviceIdle(false)
.setRequiresBatteryNotLow(false)
*/
.build()
У expedited job больший приоритет на ускоренный запуск, поэтому операционная система строже регулирует их количество. Например, если попытаться запланировать джобу при исчерпаном лимите, то сразу вернётся JobScheduler#RESULT_FAILURE.
Если же ограничения по квоте, сети и памяти устройства выполнены, то у джобы будет около минуты на выполнение своих функций. Иногда больше, но это сильно зависит от лимита и общей загруженности системы.
Миграция foreground service на expedited job
Стандартный сервис для выполнения фоновых задач обычно выглядит примерно так:
class ExampleService : Service() {
override fun onBind(intent: Intent?): IBinder? {
return null
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val notification: Notification = createNotification()
startForeground(UPLOAD_ID, notification)
runHeavyJob()
return START_NOT_STICKY
}
private fun createNotification(): Notification {
val pendingIntent: PendingIntent =
Intent(this, MainActivity::class.java).let { notificationIntent ->
PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE)
}
val nb = NotificationCompat.Builder(this, createNotificationChannel())
return buildNotification(nb)
}
private fun runHeavyJob() {
//some great stuff
}
}
А запускается так:
private fun startHeavyTask() {
Intent(this, ExampleService::class.java).also { intent ->
startService(intent)
}
}
Поговорим о том, как перевести этот сервис на expedited job. Происходит это буквально в три простых шага.
1. Подключаем WorkManager к проекту:
implementation 'androidx.work:work-runtime:2.7.0-alpha04'
2. Создаём класс, наследующийся от Worker (он будет выполнять задачу, которую раньше делал сервис):
class ExampleWorker(appContext: Context, workerParams: WorkerParameters) :
Worker(appContext, workerParams) {
override fun doWork(): Result {
runHeavyTask()
return Result.success()
}
@SuppressLint("CheckResult")
private fun runHeavyTask() {
//some great stuff
}
}
3. Создаём WorkRequest и передаём его для запуска в WorkManager:
fun runHeavyWork() {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresStorageNotLow(true)
.build()
val heavyWorkRequest: WorkRequest =
OneTimeWorkRequest.Builder(ExampleWorker::class.java)
.setConstraints(constraints) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager
.getInstance(context)
.enqueue(heavyWorkRequest)
}
Здесь есть важный параметр OutOfQuotaPolicy, который отвечает за поведение при невозможности запустить джобу немедленно. Он существует в двух вариантах:
-
RUN_AS_NON_EXPEDITED_WORK_REQUEST — при заполненной квоте запустится обычная джоба, не expedited.
-
DROP_WORK_REQUEST — при заполненной квоте запрос на выполнение сразу зафейлится.
На этом, собственно, базовая миграция заканчивается.
Вместо заключения
Переехать на expedited job довольно легко, особенно, если в проекте уже подключен WorkManager.
Сейчас пропала необходимость держать нотификацию в статус-баре, а в условиях выполнения задачи появилась дополнительная гибкость благодаря возможностям WorkManager. Например, теперь можно пережить «смерть» процесса, тонко настраивать ретраи, периодичность выполнения задач и многое другое.
Комментариев нет:
Отправить комментарий