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

Продолжаем курс по обучению основам разработки мобильных приложений в Android Studio на языке Kotlin. 

Coroutines Kotlin VS RxJava в асинхронном коде

Думаю, для тех, кто не знаком с Kotlin, стоит сказать пару слов о нем и корутинах в частности. Об актуальности изучения Kotlin говорит то, что в мае 2017 года компания Google сделала его официальным языком разработки Android.

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

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

В контексте Android в задачах обеспечения асинхронности их смело можно рассматривать как конкурента RxJava. Несмотря на то, что возможности RxJava гораздо шире (это довольно объемная библиотека со своим подходом и философией), работать с корутинами удобнее, потому что они — всего лишь часть языка программирования. Задачи, решенные на RxJava с помощью операторов (специальных методов библиотеки), на корутинах реализуются намного проще — через встроенные средства языка. К тому же операторы библиотек нужно не только знать, но и понимать, как они работают, правильно выбирать и применять. Конечно, средства языка знать и правильно применять тоже нужно, но, когда речь идет о сокращении времени на разработку, стоит задуматься, насколько изучение возможности библиотеки, которую используешь для решения небольшой задачи, актуально в сравнении с изучением языка, на котором пишется весь проект.

Null или не null?

Null safety — одна из ключевых особенностей Kotlin. Язык гарантирует, что программист не сможет по ошибке вызвать методы объекта, имеющего значение null, или передать этот объект в качестве аргумента другим методам. На практике это означает, что система типов Kotlin разделена на две ветви, в одной из которых существуют nullable-типы (со знаком вопроса на конце), а в другой — типы, у которых может быть только имеющее смысл значение.

Читайте также:  Обзор лучших программ-конвертеров видео для Android онлайн

Ты можешь написать такой код:

var string: String? = null

Но не такой:

var string: String = null

Не nullable-тип String просто не может содержать значение null.

Язык имеет ряд операторов для удобной и надежной работы с nullable-типами:

// Присвоить переменной length значение одноименного свойства string1 либо null val length = string1?.length // Выполнить код в блоке, только если string1 не null string1?.let { (string1) } // Заверить компилятор, что в данный момент значение string1 не может быть null string1!!.length // Объявить переменную не nullable-типа и заверить компилятор, // что она будет проинициализирована позже, до первого использования lateinit var recyclerView: RecyclerView

В целом удобная и понятная система. Но есть оператор, который может сделать код еще более компактным и красивым. Оператор Elvis вычисляет выражение слева и, если его результат null, вычисляет выражение справа. Он особенно удобен в двух случаях. Во-первых, его можно использовать, чтобы присвоить переменной дефолтное значение:

val name: String = ?: «unknown»

Во-вторых, чтобы вернуть управление из функции, если значение определенной переменной равно null:

fun addPerson(person: Person) { val name = ?: return }

Но есть и подводные камни. Допустим, у нас есть следующий код:

data?.let { updateData(data) } ?: run { showLoadingSpinner() }

Может показаться, что этот код делает то же самое, что и такой код:

if (data != null) { updateData(data) } else { showLoadingSpinner() }

Но это не так. Последний пример кода полностью бинарный: либо первый блок, либо второй. А вот в предыдущем фрагменте кода могут быть выполнены оба блока! Это возможно, если функция updateData(data) сама вернет null. Тогда все выражение слева вернет null и будет выполнен код справа от оператора Elvis. Обойти эту проблему можно, заменив let на apply.

Еще одна вещь, которую следует помнить о null safety, — это автоматическое выведение типов. Компилятор (и плагин среды разработки) Kotlin достаточно умен, чтобы понять тип переменной даже в самых сложных случаях. Но иногда он дает сбои.

Такое может быть при параллельной обработке данных. Возьмем следующий пример:

Читайте также:  Как отключить от Wi-Fi постороннего пользователя

class Player(var tune: Tune? = null) { fun play() { if (tune != null) { () } } }

Среда разработки сообщит, что не выведет тип tune, потому что это изменяемое свойство. Компилятор не может быть уверен, что между проверкой tune на null и вызовом метода play() другой поток не сделает tune = null.

Чтобы это исправить, достаточно сделать так:

class Player(var tune: Tune? = null) { fun play() { tune?.play() } }

Или так:

class Player(var tune: Tune? = null) { fun play() { tune?.let { val success = () } } }

Сбои могут возникать и при взаимодействии с кодом на Java. В Java понятия null safety нет, поэтому компилятор не может знать тип переменой наверняка. Kotlin решает эту проблему двумя способами:

  1. Компилятор Kotlin поддерживает практически все разновидности nullable-аннотаций, поэтому, если код аннотирован с помощью @NotNull и ему подобных аннотаций, компилятор будет считать, что аннотированная переменная не может быть null.
  2. Для взаимодействия с Java (и другими языками для JVM) в Kotlin есть специальные типы с восклицательным знаком на конце (например, String!, Integer!). Это так называемые platform types, и при работе с ними Kotlin использует ту же логику, что и Java. Однако ее можно изменить, если указать тип напрямую:

    val docBuilderFactory: DocumentBuilderFactory = ()

Далее DocumentBuilderFactory будет считаться не nullable-переменной.

Типы Unit, Nothing, Any в Kotlin

Система типов Kotlin несколько отличается от системы типов Java и может вызвать у незнающего человека много вопросов. Наиболее проблемными обычно оказываются типы Unit и Nothing.

  • Unit — эквивалент типа void в Java. Другими словами, он нужен для того, чтобы показать, что функция ничего не возвращает. Unit наследуется от типа Any, а при работе с Java-кодом автоматически транслируется в void.

  • Nothing — субкласс любого класса (именно так), не позволяющий создать объект своего типа (конструктор приватный). Используется для представления результата исполнения функции, которая никогда не завершается (например, потому что она выбрасывает исключение). Пример:

    public inline fun TODO(): Nothing = throw NotImplementedError() fun determineWinner(): Player = TODO()

  • Any — родитель всех остальных классов. Аналог Object в Java.

Создание Activity на Kotlin

В Android Studio нажмите правой кнопкой мыши на имени вашего пакета и выберите New > Kotlin File.

Создание Activity на Kotlin

В диалоговом окне, введите имя новой Activity и выберите Class из выпадающего списка. Я назвал свой класс MainActivity.

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

Создание Activity на Kotlin

В алерте нажмте на ссылку и во всплывающем окне нажмите OK для выбора значений по умолчанию.

Для настройки поддержки Kotlin в вашем проекте, плагин Kotlin сделает некоторые изменения в файле . Примените изменения настроек нажатием на кнопку Sync Now сверху.

Создание Activity на Kotlin

На этом шаге настройка проекта завершена. Вернитесь к вашему Kotlin-классу для начала кодинга.

Схема Lifecycle

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

В прямоугольниках указаны состояния (states) активити. А события (events), которые происходят при смене состояний, отмечены стрелками. Важно понимать, что не события управляют состояниями активити, скорее наоборот. Состояния активити изменяются системой Андроид, а события происходят в процессе изменения.

Когда активити запускается системой, оно инициализируется (INITIALIZED) и происходит событие ON_CREATE. При этом активити переходит к состоянию “создано” (CREATED).В этот момент должен инициализироваться пользовательский интерфейс, поскольку активити готовится отобразиться пользователю. Далее происходит событие ON_START и активити переходит к состоянию “запущено” (STARTED). Следующее событие ON_RESUME. Активити переходит в состояние — RESUMED (возобновлено) — выходит на передний план, получает фокус и отображается пользователю. Если активити в процессе работы теряет фокус и частично перекрывается, например, диалоговым окном или другим активити, то переходит обратно в состояние STARTED. При этом происходит событие ON_PAUSE. В этом состоянии активити приостанавливается, но может быть все еще видимым на экране, например, в многооконном режиме. Если же активити полностью перекрыто, то система его останавливает и переводит в состояние CREATED. Выполняется событие ON_STOP. Активити пока не уничтожается системой и может в любой момент возобновить работу. Но поскольку оно не видно пользователю, то в этом состоянии целесообразно отключать некоторые функции, например, воспроизведение анимации. Если пользователь закрыл активити или система испытывает недостаток памяти, или изменилась конфигурация устройства (произошел поворот), активити уничтожается системой. При этом происходит событие ON_DESTROY. В этот момент необходимо освобождать ресурсы, используемые активити.