Background service android kotlin

Руководство по фоновой работе в Android. Часть 5: Корутины в Котлине


Остров Котлин

Итак, этот час настал. Это статья, ради которой была написана вся серия: объяснение, как новый подход работает «под капотом». Если вы пока не знаете и того, как им пользоваться, вот для начала полезные ссылки:

  • Страница на официальном сайте
  • Блог-пост Android Coroutine Recipes
  • Доклад Романа Елизарова

А освоившись с корутинами, вы можете задаться вопросом, что позволило Kotlin предоставить эту возможность и как она работает. Прошу заметить, что здесь речь пойдёт только о стадии компиляции: про исполнение можно написать отдельную статью.

Первое, что нам нужно понять — в рантайме вообще-то не существует никаких корутин. Компилятор превращает функцию с модификатором suspend в функцию с параметром Continuation. У этого интерфейса есть два метода:

Тип T — это тип возвращаемого значения вашей исходной suspend-функции. И вот что на самом деле происходит: эта функция выполняется в определённом потоке (терпение, до этого тоже доберёмся), и результат передаётся в resume-функцию того continuation, в контексте которого вызывалась suspend-функция. Если функция не получает результат и выбрасывает исключение, то вызывается resumeWithException, пробрасывая ошибку вызывавшему коду.

Хорошо, но откуда взялось continuation? Разумеется, из корутиновского builder! Давайте посмотрим на код, создающий любую корутину, к примеру, launch:

Тут builder создаёт корутину — экземпляр класса AbstractCoroutine, который, в свою очередь, реализует интерфейс Continuation. Метод start принадлежит интерфейсу Job. Но найти определение метода start весьма затруднительно. Но мы можем зайти тут с другой стороны. Внимательный читатель уже заметил, что первый аргумент функции launch — это CoroutineContext, и по умолчанию ему присвоено значение DefaultDispatcher. «Диспетчеры» — это классы, управляющие исполнением корутин, так что они определённо важны для понимания происходящего. Давайте посмотрим на объявление DefaultDispatcher:

Так что, по сути, это CommonPool, хотя java-доки и говорят нам, что это может измениться. А что такое CommonPool?

Это диспетчер корутин, использующий ForkJoinPool в качестве реализации ExecutorService. Да, это так: в конечном счёте все ваши лямбда-корутины — это просто Runnable, попавшие в Executor с набором хитрых трансформаций. Но дьявол как всегда в мелочах.


Fork? Или join?

Судя по результатам опроса в моём твиттере, тут требуется вкратце объяснить, что представляет собой FJP 🙂

While I am working on the 5th part of the «#Android background in a nutshell», I must ask you: how familiar you are with ForkJoinPool? RT for bigger audience

В первую очередь, ForkJoinPool — это современный executor, созданный для использования с параллельными стримами Java 8. Оригинальная задача была в эффективном параллелизме при работе со Stream API, что по сути означает разделение потоков для обработки части данных и последующее объединение, когда все данные обработы. Упрощая, представим, что у вас есть следующий код:

Сумма такого стрима не будет вычислена в одном потоке, вместо этого ForkJoinPool рекурсивно разобьёт диапазон на части (сначала на две части по 500 000, затем каждую из них на 250 000, и так далее), посчитает сумму каждой части, и объединит результаты в единую сумму. Вот визуализация такого процесса:


Потоки разделяются для разных задач и вновь объединяются после завершения

Эффективность FJP основана на алгоритме «похищения работы»: когда у конкретного потока кончаются задачи, он отправляется в очереди других потоков пула и похищает их задачи. Для лучшего понимания можно посмотреть доклад Алексея Шипилёва или проглядеть презентацию.

Читайте также:  Mi fit для андроид инструкция

Отлично, мы поняли, что выполняет наши корутины! Но как они там оказываются?

Это происходит внутри метода CommonPool#dispatch:

Метод dispatch вызывается из метода resume (Value: T) в DispatchedContinuation. Звучит знакомо! Мы помним, что Continuation — это интерфейс, реализованный в AbstractCoroutine. Но как они связаны?

Трюк заключён внутри класса CoroutineDispatcher. Он реализует интерфейс ContinuationInterceptor следующим образом:

Видите? Вы предоставляете в builder корутин простой блок. Вам не требуется реализовывать никакие интерфейсы, о которых вы знать ничего не хотите. Это всё берёт на себя библиотека корутин. Она
перехватывает исполнение, заменяет continuation на DispatchedContinuation, и отправляет его в executor, который гарантирует наиболее эффективное выполнение вашего кода.

Теперь единственное, с чем нам осталось разобраться — как dispatch вызывается из метода start. Давайте восполним этот пробел. Метод resume вызывается из startCoroutine в extension-функции блока:

А startCoroutine, в свою очередь, вызывается оператором «()» в перечислении CoroutineStart. Ваш builder принимает его вторым параметром, и по умолчанию это CoroutineStart.DEFAULT. Вот и всё!

Вот по какой причине меня восхищает подход корутин: это не только эффектный синтаксис, но и гениальная реализация.

А тем, кто дочитал до конца, достаётся эксклюзив: видеозапись моего доклада «Скрипач не нужен: отказываемся от RxJava в пользу корутин в Котлине» с конференции Mobius. Наслаждайтесь 🙂

Источник

Фоновая работа в Android: обзор возможностей WorkManager

В мобильных приложениях широко востребованы различные виды фоновой работы. Зачастую нужно поддерживать работу в офлайне, планировать какие-либо долгие и повторяющиеся задачи на определенное время, выполнять «тяжелые» задачи без привязки к сценариям пользовательского взаимодействия.

Например, в ритейле мерчендайзерам бывает необходимо в конце каждого рабочего дня отправлять фотоотчеты на сервер и удалять их из памяти телефона, чтобы не занимать место. А для работы онлайн-кассы требуется в фоновом режиме загружать актуальный справочник товаров. В этой статье мы рассмотрим один из самых популярных инструментов для реализации фоновой работы – WorkManager из Android Jetpack.

Для фоновой работы в Android существует множество изначально реализованных нативных решений, таких как AlarmManager, Handler, IntentService, SyncAdapter, Loader. Однако, их судьба складывается по-разному:

Handler до сих пор повсеместно используется, но, в основном, для посылки эвентов в очередь событий главного потока.

На действия AlarmManager система Android накладывает все больше ограничений, также он имеет довольно раздутое API для работы.

IntentService, используемый для обработки операций в рабочем потоке, начиная с Android API 30 стал deprecated.

Loader имеет привязку к жизненному циклу Activity/Fragment и, с появлением более новых, удобных инструментов, позволяющих решать схожие задачи, морально устарел.

SyncAdapter также морально устарел, не имеет возможности задавать условия запуска задач, создавать цепочки задач.

Начиная с Android 5.0 появился JobScheduler, позволяющий задавать условия запуска задач (устройство на зарядке, подключено к wi-fi и т.д.). Его работа основана на Service, и, чтобы обработка прошла асинхронно, необходимо самостоятельно запустить рабочий поток, а также вызвать необходимые методы JobService для избежания утечек. Все описанное повышает вероятность ошибок и доступно только с api 21.

Учитывая перечисленные ограничения, разработчики столкнулись с потребностью в таком инструменте, который может инкапсулировать работу по избежанию утечек и обращению с потоками, а также предоставить удобное API для запуска асинхронных, повторяющихся, откладываемых задач. В результате в 2018 году был выпущен Android Jetpack, частью которого стал WorkManager (познакомиться с ним подробнее можно, в частности, здесь).

Далее рассмотрим подробнее особенности работы.

WorkManager предоставляет удобные инструменты для описанных выше задач, совместим с корутинами, RxJava2, другими Jetpack библиотеками, может работать в мультипроцессном режиме. Доступен он начиная с API 14 за счет использования под капотом уже знакомых инструментов.

Читайте также:  Bakugan battle brawlers android nds

1) Описание и добавление задачи

Для описания задачи необходимо унаследоваться от класса Worker и определить метод doWork():

Код внутри метода doWork() будет выполнен в рабочем потоке WorkManager’a.

Далее задачу можно сделать разовой с помощью OneTimeWorkRequestBuilder.

Либо ее можно сделать периодической с помощью PeriodicWorkRequestBuilder.

В обоих случаях мы передали в качестве generic-параметра класс Worker’a, определенный нами.

В случае периодической задачи мы дополнительно определили интервал ее выполнения — 30 минут (минимально доступный интервал составляет 15 минут; если мы поставим интервал меньше 15 минут, то WorkManager повысит его до 15). А также параметр flex — 25 минут. Этот параметр ограничивает окно запуска задачи: она будет запущена не в любой момент интервала, а между 25 и 30 минутами.

Также для задачи можно установить начальную задержку и тег, по которому задача однозначно будет найдена в списке запланированных задач.

После создания задачи мы добавляем ее в очередь WorkManager’a.

2) Критерии запуска задачи

Мы можем задать необходимые условия для запуска задачи:

После этого констрейнты добавляются в билдере work request’a.

Рассмотрим перечисленные условия запуска:

setRequiresCharging (boolean requiresCharging) — критерий: зарядное устройство должно быть подключено.

setRequiresBatteryNotLow (boolean requiresBatteryNotLow) — критерий: уровень батареи не ниже критического (задача начинает выполняться при уровне заряда больше 20, а останавливается при значении меньше 16).

setRequiredNetworkType (NetworkType networkType) — критерий: наличие интернета. Мы можем указать, какой именно тип сети интернет (NetworkType) должен быть при запуске задачи. Тип соединения с сетью может быть:

CONNECTED — WiFi или Mobile Data

UNMETERD — только WiFi

METERED — только Mobile Data

NOT_ROAMING — интернет не должен быть роуминговым;

NOT_REQUIRED — интернет не нужен.

setRequiresDeviceIdle (boolean requiresDeviceIdle) — критерий: девайс не используется какое-то время и ушел “в спячку”. Работает на API 23 и выше.

setRequiresStorageNotLow (boolean requiresStorageNotLow) — критерий: на девайсе должно быть свободное место, не меньше критического порога.

3) Цепочки задач

WorkManager может выполнять несколько разовых задач последовательно друг за другом. При этом задача, для которой не выполнены критерии запуска, будет ожидать их соблюдения вместе с теми, что идут после нее. Если порядок задач не важен, можно запустить их параллельно.

Периодические задачи ставить в цепочку нельзя.

В данном примере мы параллельно запускаем задачи myWorkRequest1, myWorkRequest2. После их выполнения будут параллельно выполняться задачи myWorkRequest3, myWorkRequest4. Затем — задача myWorkRequest5. Данный пример можно переписать, выделив параллельные запросы в цепочки. После чего две получившиеся цепочки можно передать в метод combine() класса WorkContinuation для параллельного исполнения:

4) Уникальная цепочка задач

Мы можем сделать последовательность задач уникальной. Для этого нужно начать последовательность методом beginUniqueWork():

В этот метод мы передали название уникальной цепочки, стратегию действий при коллизии цепочек с одинаковым именем и саму задачу (можно передать список задач).

Стратегий может быть несколько:

REPLACE – остановка последовательности с таким же именем, и запуск новой;

KEEP – оставит в работе текущую выполняемую последовательность, а новая будет проигнорирована;

APPEND – запустит новую последовательность после выполнения текущей.

5) Отмена задачи

Для отмены задачи у класса WorkManager есть следующие методы:

cancelAllWork() — отменяет все запланированные задачи (и не только вашим приложением);

cancelAllWorkByTag(String tag) — отменяет все задачи с указанным тегом;

cancelUniqueWork(String uniqueWorkName) — отменяет уникальную цепочку задач с указанным именем;

cancelWorkById(UUID id) — отменяет задачу по указанному id.

6) Статусы задач

Статусы и жизненный цикл задачи может быть различным, в зависимости от того, разовая она или нет. Возможны следующие статусы задач:

Читайте также:  Кнопка старт для андроид

ENQUEUED – задача запланирована;

RUNNING – задача выполняется;

SUCCEEDED (SUCCESS) – задача выполнена, терминальное состояние;

FAILED (FAILURE) – задача не была выполнена, не повторять, терминальное состояние;

RETRY – задача не была выполнена, повторить через некоторое время;

BLOCKED – задача включена в цепочку, и ее очередь выполнения еще не наступила;

CANCELLED – задача отменена, терминальное состояние.

Для разовой задачи возможен следующий флоу:

Если одна из разовых задач включена в цепочку и завершилась с состоянием FAILED, следующие за ней по цепочке задачи будут отменены.

Для периодической задачи существует другой флоу:

Как мы видим, для периодической задачи существует одно терминальное состояние: CANCELLED.

Так как WorkManager является частью Jetpack, получить информацию о задаче можно в виде LiveData:

7) Входные и выходные данные задачи

Мы можем задать входные данные, необходимые для ее работы.

Когда задача будет запущена, мы сможем получить эти данные внутри нее с помощью:

После выполнения мы можем в Result.success() или Result.failure() передать выходные данные задачи.

8) Передача данных между задачами

При создании цепочки задач выходные данные одной задачи будут передаваться во входные данные другой. Рассмотрим такой пример. Пусть задачи myWorkRequest1 и myWorkRequest2 выполняются параллельно, затем выполняется myWorkRequest3. В результате выходные данные из первой и второй задач попадут в третью.

Так как мы задали одинаковые ключи, невозможно предположить, какое именно значение попадет в третью задачу, ведь первая и вторая задача выполняются параллельно. Важно об этом помнить.

9) InputMerger

Для преобразования нескольких выходных результатов в один входной используются реализации класса InputMerger. По умолчанию используется OverwritingInputMerger, перезаписывающий значение по уже существующему ключу. Это было видно на предыдущем примере с двумя параллельными задачами. Если же нам нужно при совпадении ключа записать все пришедшие значения, следует использовать ArrayCreatingInputMerger.

InputMerger можно задать при создании задачи. Добавим ArrayCreatingInputMerger для myWorkRequest3 из предыдущего примера.

Теперь по ключу keyA мы получим не последнее записанное значение, а массив [«value1», «value2»]. Также и для ключа keyB — [1, 2].

10) Кастомная конфигурация WorkManager

WorkManager имеет собственный провайдер, WorkManagerInitializer, который мержится в манифест приложения. Он создает пул потоков, необходимый для работы, фабрики для создания InputMerger’ов и WorkerFactory (создает объекты Worker’ов, полезна в случаях, когда класс Worker’a получил новое имя, и WorkManager должен соотнести действия, запланированные на старое имя, с новым). При необходимости эти параметры можно изменить.

Для начала нужно отключить стандартный провайдер WorkManager’a. Это делается в манифесте приложения путем объявления.

После этого нужно сделать класс нашего приложения реализацией интерфейса Configuration.Provider. И в переопределенном методе изменить нужные нам параметры. Например, заменим стандартный Executor, в потоках которых выполняется код Worker’ов, и изменим минимальный уровень логирования:

11) Тестирование

Для тестирования Worker’ов нужно подключить в проект зависимость

В этом пакете есть классы, которые помогают в тестовой среде.

Пусть у нас есть Worker, который складывает 2 числа и выдает результат в качестве выходных данных. Протестируем его:

Дополнительно можно проверить, что код Worker’a выполняется только тогда, когда выполнены все условия его запуска. А также проверить статус работы.

Пусть у нас есть тот же Worker, выполняющий сложение 2 чисел. Зададим ему условия старта и протестируем.

Заключение

WorkManager предоставляет широкие возможности для асинхронной работы, которая должна выполняться в определенном состоянии устройства, откладывать ее, планировать, выполнять параллельно, перезапускать, запускать периодически, работать в многопроцессном режиме. Также этот инструмент гарантирует выполнение задачи даже после закрытия приложения и перезагрузки устройства. А его доступность, начиная с API 14, делает его must-have инструментом для подобного рода задач.

Спасибо за внимание! Надеемся, что этот материал был полезен для вас.

Источник

Оцените статью