
С релизом 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. Например, теперь можно пережить «смерть» процесса, тонко настраивать ретраи, периодичность выполнения задач и многое другое.
ссылка на оригинал статьи https://habr.com/ru/articles/569598/
Добавить комментарий