Применяем Kotlin Coroutines в боевом Android-проекте

Евгений Кабак, разработчик компании Программные технологии, поделился опытом тестирования корутин в рамках одного из текущих проектов.
Coroutines Kotlin VS RxJava в асинхронном коде
Думаю, для тех, кто не знаком с Kotlin, стоит сказать пару слов о нем и корутинах в частности. Об актуальности изучения Kotlin говорит то, что в мае 2017 года компания Google сделала его официальным языком разработки Android.
Проекты, стартующие в нашей компании, мы пишем на Kotlin, поэтому изучаем существующие возможности и следим за выходом новых. Когда создатели языка анонсировали корутины как новый инструмент асинхронного программирования, стало интересно протестировать их в боевых условиях. Судя по описанию возможностей, они как раз подходят для решения наших задач и отличаются в лучшую сторону от уже существующих решений.
Итак, для чего нужны корутины? Если требуется скачать что-то из сети, извлечь данные из базы данных или просто выполнить долгие вычисления и при этом не заблокировать интерфейс пользователю, можно использовать корутины. В контексте Android в задачах обеспечения асинхронности их смело можно рассматривать как конкурента RxJava. Несмотря на то, что возможности RxJava гораздо шире (это довольно объемная библиотека со своим подходом и философией), работать с корутинами удобнее, потому что они — всего лишь часть языка программирования. Задачи, решенные на RxJava с помощью операторов (специальных методов библиотеки), на корутинах реализуются намного проще — через встроенные средства языка. К тому же операторы библиотек нужно не только знать, но и понимать, как они работают, правильно выбирать и применять. Конечно, средства языка знать и правильно применять тоже нужно, но, когда речь идет о сокращении времени на разработку, стоит задуматься, насколько изучение возможности библиотеки, которую используешь для решения небольшой задачи, актуально в сравнении с изучением языка, на котором пишется весь проект.
Примеры использования Coroutines Kotlin
Поддержка корутин встроена в Kotlin, но все классы и интерфейсы находятся в отдельной библиотеке. Для их использования нужно добавить зависимость в gradle:
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.0.1' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1' }
Небольшой пример использования:
val job: Job = GlobalScope.launch(Dispatchers.IO) { longRunningMethod() }
Разберемся, что тут происходит. longRunningMethod() — метод, который нам нужно выполнить асинхронно. GlobalScope — жизненные рамки для корутины. В данном случае корутина будет жить, пока живо приложение, в котором она запущена. GlobalScope — конкретная реализация интерфейса CoroutineScope. Можно реализовать свой scope, например, в Activity, и это приведет к тому, что запущенные в Activity корутины будут автоматически отменяться в случае завершения или краша Activity. launch — метод для асинхронного запуска корутины. Соответственно, метод longRunningMethod() запустится сразу же. Метод возвращает экземпляр класса Job. Этот объект можно использовать для того, чтобы, например, отменить корутину — job.cancel(). Альтернатива — метод asunc(). Он вернет Deferred — отложенную корутину, которую можно запустить позднее. Dispatchers.IO — один из параметров метода launch(). Здесь указывается диспетчер для созданной корутины. Конкретно диспетчер Dispatchers.IO используется для фоновых задач, не блокирующих основной поток. Если указать Dispatchers.Main, то корутина будет выполняться в основном потоке. Что имеем в итоге? Простой метод запуска асинхронного кода. Но в этом кусочке кода есть скрытые преимущества, которые не видны на первый взгляд:
  • корутины легковесны. Аналогичный код с созданием и запуском потока потребует много больше памяти:
thread { longRunningMethod() }
Корутины же мы можем создавать тысячами;
  • корутину можно приостановить. Метод delay(timeout) приостановит выполнение корутины, но это никак не отразится на потоке, в котором она выполняется;
  • в отличие от RxJava, для написания асинхронного кода не надо заучивать массу операторов типа merge, zip, andThen map, flatMap и т.д. Можно просто писать код, который будет запущен асинхронно, используя минимум дополнительных методов. Для реализации более сложной логики можно применять уже знакомые языковые конструкции, такие как foreach, repeat, filter и т.д.
Когда нужен асинхронный подход
Вернемся к нашей задаче использования корутин. Приложение под Android, над которым мы сейчас работаем, общается с сервером и хранит информацию в базе данных. На первый взгляд, ничего нового по функционалу, но интересен сам подход в реализации, поскольку тестируем новый инструмент разработки асинхронного кода — корутины Kotlin.
В своем приложении мы применяем асинхронный код. Почему? Дело в том, что общение с сервером и запросы в базу данных — довольно продолжительные операции. Пока выполняется одна, вполне можно успеть завершить еще несколько, не блокируя основной поток. Именно эту задачу и решает асинхронный подход. В случае синхронного программирования операции выполняются последовательно, т.е. следующая команда запускается только после того, как завершится предыдущая, и когда какая-нибудь из них выполняется слишком долго, программа может зависнуть. И хотя все понимают, что “зависание” вряд ли понравится пользователям, все же такую реализацию иногда можно встретить в приложениях. Повторюсь, мы решаем задачу с помощью асинхронного кода.
Применение Coroutines Kotlin в нашем проекте
Итак, запуск абстрактного асинхронного кода — это хорошо, но попробуем решить более насущную задачу. Допустим, надо сделать запрос на сервер и показать результат. Посмотрим, как это можно сделать с помощью корутин. Само скачивание будем для простоты выполнять во ViewModel, общаться с Activity будем с помощью LiveData. Создадим класс-наследник ViewModel:
class LoginViewModel(application: Application) : BaseViewModel(application) { private val loginLiveData = MutableLiveData<Resource<UserProfile>>() fun getLoginLiveData(): LiveData<Resource<UserProfile>> { return loginLiveData } fun login(name: String, password: String) { runCoroutine(loginLiveData) { val response = ServerApi.restApi.authorize(name, password).execute() return@runCoroutine response.body()!! } } }
Внутри модель содержит MutableLiveData с данными пользователя, который мы получаем после авторизации. Наружу отдаем неизменяемую LiveData, чтобы никто кроме ViewModel не мог изменять данные внутри. Профиль пользователя завернут в класс Resource<>. Это утилитарный класс для удобства передачи состояния процесса во View:
data class Resource<T>( val status: Status, val data: T?, val exception: Exception? ) { enum class Status { LOADING, COMPLETED } }
Как видно, во View мы можем передавать информацию о том, завершилось ли скачивание, и если завершилось, то с ошибкой или успешно. Запуск корутины происходит в методе login(). Он вызывает метод базового класса runCoroutine() :
protected fun <T> runCoroutine(correspondenceLiveData: MutableLiveData<Resource<T>>, block: suspend () -> T) { correspondenceLiveData.value = Resource(Resource.Status.LOADING, null, null) GlobalScope.launch(Dispatchers.IO) { try { val result = block() correspondenceLiveData.postValue(Resource(Resource.Status.COMPLETED, result, null)) } catch (exception: Exception) { val error = ErrorConverter.convertError(exception) correspondenceLiveData.postValue(Resource(Resource.Status.COMPLETED, null, error)) } } }
У этого метода 2 параметра. Первый — типизированный экземпляр LiveData, куда будут записаны данные. Второй — код, который нужно выполнить асинхронно. В методе login() мы передаем код, который передает на сервер данные для авторизации и получает от сервера профиль пользователя. Как работает все вместе: View получает от ViewModel LiveData, подписывается на ее изменения. Изменения могут быть трех видов: идет какой-то процесс, все завершилось с ошибкой, все завершилось успешно. В нужный момент вызывается метод login(). Затем последовательно происходит: запись в LiveData информации о том, что “идет какой-то процесс”, запрос на сервер, получение данных, запись в LiveData полученных данных. Или ошибки, если запрос на сервер не удался.
Выводы
Естественно, в одной статье невозможно раскрыть все аспекты нового инструмента в асинхронном программировании. Тем, кто заинтересовался корутинами Kotlin, можно посоветовать изучить и протестировать, к примеру, комбинирование корутин, каналы, реализацию Actor model и другие возможности. Несмотря на то, что в рамках примера показана довольно банальная задача — скачать данные с сервера, что делается почти в каждом приложении, он иллюстрирует принцип работы с корутинами. Вместо скачивания может быть что угодно. Пример показывает, как с помощью корутин удобно обернуть любую операцию. Как мы видим, задачи асинхронного программирования под Android проще реализовать с помощью корутин: быстрее разработка, выше читаемость кода. Риски, конечно, тоже есть: для изучения корутин нужно некоторое время, а их возможности иногда могут не закрыть все требования задачи. Перед использованием в своем проекте рекомендуется внимательно ознакомиться с областью их применения. Официальный сайт разработчиков Kotlin: http://kotlinlang.org/ Анонс релиза Kotlin 1.3.0 с Coroutines: https://blog.jetbrains.com/kotlin/2018/10/kotlin-1-3/
Опубликовано:
07.02.2020
Автор:
Анна Терехина
Поделиться