Главная » Хабрахабр » [Перевод] Изучаем KTX для Android

[Перевод] Изучаем KTX для Android

Привет, «Хабр»! Прошло почти 9 месяцев с тех пор, как на Google I/O 2017 компания Google анонсировала Kotlin в качестве официального языка разработки под Android. Кто-то использует его как основной инструмент намного дольше, учитывая, что на нём можно было писать уже с середины 2014 года. В документации Google стали появляться примеры реализации на Kotlin. За это время разработчики смогли «потрогать» и оценить все преимущества этого языка. И многие, включая меня, думали: какой же шаг будет следующим? Support Library на Kotlin? Или что-то новое? И вот, встречайте: Android KTX! А мы представляем вашему вниманию перевод статьи, посвящённой его разбору.


5 февраля Google анонсировала выход библиотеки Android KTX, которая представляет собой набор расширений Kotlin для разработки приложений под Android. Этот шаг выглядит как логическое продолжение интеграции Kotlin в Android-разработку и добавление всех его преимуществ: меньше кода, больше удовольствия и облегчение понимания кода проекта.

Сейчас библиотека доступна только в preview и открыта для новых идей. Скорее всего, в ней будет значительно больше фичей и возможностей, когда её выведут в стабильную версию. Я писал эту статью, одновременно проглядывая документацию, и подумал, что хорошо было бы подробнее рассмотреть некоторые моменты. Вероятнее всего, вы сами начнёте изучать документацию лишь тогда, когда вам это понадобится. Но я надеюсь, что эта статья даст вам представление о том, что можно использовать уже сейчас. И если вы всё ещё не используете Kotlin, то поймёте, что вы упускаете.

Документацию по KTX вы можете найти здесь: core-ktx и саму библиотеку здесь: android/android-ktx.

Здесь собраны функции расширения, относящиеся к анимации. Давайте быстро пробежимся по тому, что доступно в текущем релизе.

Animation listener

Для начала установим animation listener на animator:

animator.addListener { handleAnimation(it) }

Эта конструкция позволяет нам получать колбэки для событий анимации. Мы также можем использовать функции расширения для определённых колбэков от listener, нужно только реализовать функции, которые вы хотите получать:

animator.addListener( onEnd = {}, onStart = {}, onCancel = {}, onRepeat = {}
)

Это солидное уменьшение кода за счёт отсутствия реализации тех колбэков, в которых мы не нуждаемся и которые не используем.

Отдельные listeners для событий анимации

У нас появилась возможность устанавливать listener на отдельные события: например, добавить listener на событие pause можно так же, как и функцию addListener():

animator.addPauseListener { handleAnimation(it) }
// или
animator.addPauseListener( onPause = {}, onResume = {}
)

Мы также можем «повесить» listener на событие анимации с помощью однострочного синтаксиса:

animator.doOnPause { handleAnimation(it) }
animator.doOnCancel { handleAnimation(it) }
animator.doOnEnd { handleAnimation(it) }
animator.doOnRepeat { handleAnimation(it) }
animator.doOnStart { handleAnimation(it) }
animator.doOnResume { handleAnimation(it) }

Если сейчас вы используете Java, то заметите, насколько меньше кода требуется для реализации и насколько легче это всё читается.

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

val alarmManager = systemService<AlarmManager>()

Styled attributes также будут работать при использовании функций расширения:

context.withStyledAttributes(set = someAttributeSet, attrs = attributes, defStyleAttr = ..., defStyleRes = ...) { // Какие-то действия
}
context.withStyledAttributes(set = someAttributeSet, attrs =
attributes) { // Какие-то действия
}

Выполнить операции записи для SharedPreferences теперь максимально просто с использованием функции расширения edit:

sharedPreferences.edit { putBoolean(key, value)
}

Мы также можем создать экземпляр ContentValues, используя функцию contentValuesOf, передав в качестве аргументов экземпляры Pair:

val contentValues = contentValuesOf(somePairs...)

KTX также предлагает использовать методы, относящиеся к работе со временем. Посмотрим, что мы здесь имеем.
Теперь можно получить DayOfWeek, Month и Year как Int-значение простым вызовом:

DayOfWeek.FRIDAY.asInt()
Month.APRIL.asInt()
Year.now().asInt()

Класс Duration также имеет несколько доступных функций расширения:

// Получение значений
val (seconds, nanoseconds) = Duration.ofSeconds(1)
// умножение
val resultValue = Duration.ofSeconds(1) * 2
// Деление
val resultValue = Duration.ofSeconds(2) / 2
// Инверсия
val resultValue = -Duration.ofSeconds(2)

Свойства Instant, LocalData, LocalDateTime, LocalTime могут быть получены следующими функциями расширения:

// Получение значений
val (seconds, nanoseconds) = Instant.now()
// Получение значений
val (year, month, day) = LocalDate.now()
// Получение значений
val (localDate, localTime) = LocalDateTime.now()
// Получение значений
val (hour, minute, second, nanosecond) = LocalTime.now()

Так же, как в методах, перечисленных выше, доступ к свойствам классов MonthDay, OffsetDateTime и OffsetTime может быть получен через вызовы следующих методов:

// Получение значений
val (month, day) = MonthDay.now()
// Получение значений
val (localDataTime, ZoneOffset) = OffsetDateTime.now()
// Получение значений
val (localTime, ZoneOffset) = OffsetTime.now()

Если вы используете класс Period, то библиотека KTX содержит несколько функций расширения для доступа к свойствам и операциям этого класса:

// Получение значений
val (years, month, days) = Period.ofDays(2)
// Умножение
val resultValue = Period.ofDays(2) * 2
// Инверсия
val resultValue = -Period.ofDays(2)

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

// Получение значений
val (year, month) = YearMonth.now()
// Получение значений
val (localDateTime, ZoneId) = ZonedDateTime.now()

Следующие функции расширения являются действительно хорошим дополнением и позволяют нам легко перевести Int-значение в необходимый класс, предусмотренный вызовом функции:

someInt.asDayOfWeek() // возвращает экземпляр DayOfWeek
someInt.asMonth() // возвращает экземпляр Month
someInt.asYear() // возвращает экземпляр Year
someInt.days() // возвращает экземпляр Period
someInt.hours() // возвращает экземпляр Duration
someInt.millis() // возвращает экземпляр Duration
someInt.minutes() // возвращает экземпляр Duration
someInt.months() // возвращает экземпляр Period
someInt.nanos() // возвращает экземпляр Duration
someInt.seconds() // возвращает экземпляр Duration
someInt.years() // возвращает экземпляр Period

Так же это работает для Long-значений:

someLong.asEpochMillis() // возвращает экземпляр Instant
someLong.asEpochSeconds() // возвращает экземпляр Instant
someLong.hours() // возвращает экземпляр Duration
someLong.millis() // возвращает экземпляр Duration
someLong.minutes() // возвращает экземпляр Duration
someLong.nanos() // возвращает экземпляр Duration
someLong.seconds() // возвращает экземпляр Duration

Здесь собраны функции расширения, направленные на взаимодействие с пакетом Android OS.
Они включают в себя несколько функций расширения для работы с классом Handler:

handler.postAtTime(uptimeMillis = 200L) { // Какое-то действие
}
handler.postDelayed(delayInMillis = 200L) { // Какое-то действие
}

Создание экземпляра класса Bundle сейчас выглядит намного приятнее:

val bundle = bundleOf("some_key" to 12, "another_key" to 15)
val bundle = persistableBundleOf("some_key" to 12, "another_key" to 15)

И если вы записываете trace events для Systrace tool, запись сообщений trace будет проще и красивее:

trace("section_name") { }

В пакете Util собраны функции расширения для работы с файлами, массивами и другими основными типами данных.
Если вы работаете с AtomicFiles, то вы можете использовать следующие функции:

val fileBytes = atomicFile.readBytes()
val text = atomicFile.readText(charset = Charset.defaultCharset())
atomicFile.tryWrite { // Ваша операция записи
}
atomicFile.writeBytes(byteArrayOf())
atomicFile.writeText("some string", charset = Charset.defaultCharset())

Для LongSparseArray, SparseArray, SparseBooleanArray, SparseIntArray, SparseLongArray типов нам стали доступны:

array.contains(someKey)
array.containsKey(someKey)
array.containsValue(someValue)
array.forEach { key, value -> doSomething(key, value) }
array.getOrDefault(key = keyValue, defaultValue = defaultValue)
array.getOrElse(key = keyValue, defaultValue = defaultValue)
array.isEmpty()
array.isNotEmpty()
val keyIterator = array.keyIterator()
val valueIterator = array.valueIterator()
array.plus(anotherArray)
array.putAll(anotherArray)
array.remove(key = keyValue, value = value)
array.set(key = keyValue, value = value)
array.size

Работа с классом Pair стала немного легче:

val pair = android.util.Pair("dsfn", "sdihfg")
// Получение значений
val (key, value) = pair
// Конвертирование Android Pair в Kotlin Pair
val kotlinPair = pair.toKotlinPair()

Мы также можем конвертировать Kotlin Pair в Android Pair:

val pair = Pair("dsfn", "sdihfg")
val androidPair = pair.toAndroidPair()

Если вы работаете с классом Half, то благодаря KTX стало проще конвертировать в него данные других типов:

short.toHalf()
string.toHalf()
float.toHalf()
double.toHalf()

Используя функции расширения, теперь можно преобразовать экземпляр класса ClosedRange в Range:

val range = closedRange.toRange()

Над экземпляром класса Range появилась возможность выполнять следующие действия:

val range = closedRange.toClosedRange()
// Возвращает пересечение двух диапазонов
val resultValue = closedRange and someOtherRange
// Возвращает наименьший диапазон, включающий два диапазона
val resultValue = closedRange += someOtherCloseRange
// Возвращает пересечение диапазона и заданного значения
val resultValue = closedRange += someValue

Оба класса Size и SizeF могут использовать функции расширения:

val size = Size(5, 5)
// Получение значений
val (width, height) = size

В данном разделе собраны функции расширения, доступные для класса Cursor. Каждая группа функций расположена в следующем порядке:

  • первая функция возвращает тип non-null, используя заданное имя столбца;
  • вторая функция возвращает тип данных (или null), используя заданное имя столбца;
  • третья функция возвращает тип данных (или null), используя заданный индекс.
cursor.getBlob(columnName = "some_column")
cursor.getBlobOrNull(columnName = "some_column")
cursor.getBlobOrNull(index = 0) cursor.getDouble(columnName = "some_column")
cursor.getDoubleOrNull(columnName = "some_column")
cursor.getDoubleOrNull(index = 0) cursor.getFloat(columnName = "some_column")
cursor.getFloatOrNull(columnName = "some_column")
cursor.getFloatOrNull(index = 0) cursor.getInt(columnName = "some_column")
cursor.getIntOrNull(columnName = "some_column")
cursor.getIntOrNull(index = 0) cursor.getLong(columnName = "some_column")
cursor.getLongOrNull(columnName = "some_column")
cursor.getLongOrNull(index = 0) cursor.getShort(columnName = "some_column")
cursor.getShortOrNull(columnName = "some_column")
cursor.getShortOrNull(index = 0) cursor.getString(columnName = "some_column")
cursor.getStringOrNull(columnName = "some_column")
cursor.getStringOrNull(index = 0)

В данный момент для SQLite есть всего одна функция, но зато очень полезная. Она позволяет нам производить транзакции, используя заданные SQL-операторы.

sqLiteDatabase.transaction { "some SQL statement" }

Что касается ресурсов, то пока добавлены только те функции расширения, которые упрощают работу с классом TypedArray.

val boolean = typedArray.getBooleanOrThrow(0)
val int = typedArray.getColorOrThrow(0)
val colorStateList = typedArray.getColorStateListOrThrow(0)
val float = typedArray.getDimensionOrThrow(0)
val int = typedArray.getDimensionPixelOffsetOrThrow(0)
val int = typedArray.getDimensionPixelSizeOrThrow(0)
val drawable = typedArray.getDrawableOrThrow(0)
val float = typedArray.getFloatOrThrow(0)
val typeface = typedArray.getFontOrThrow(0)
val int = typedArray.getIntOrThrow(0)
val int = typedArray.getIntegerOrThrow(0)
val string = typedArray.getStringOrThrow(0)
val charSequenceArray = typedArray.getTextArrayOrThrow(0)
val charSequence = typedArray.getTextOrThrow(0)

Примечание: Все throw пробрасывают IllegalArgumentException, если указанный индекс не существует.

Большинство приложений, над которыми мы (разработчики) работаем, используют текст в разным местах этих самых приложений. К счастью, в KTX есть несколько функций для работы с ним, в частности для класса SpannableStringBuilder.
Например, сразу после инициализации билдера мы можем использовать эти функции, чтобы добавить некоторый жирный текст в конец исходной строки:

val builder = SpannableStringBuilder(urlString) .bold { append("hi there") }
// или bold / italic / underlined текст, если хотите.
val builder = SpannableStringBuilder(urlString) .bold { italic { underline { append("hi there") } } } 

Также есть build-функции, которые могут установить цвет фона или добавить отступы в текст:

.backgroundColor(color = R.color.black) { // Действие в builder
}
.inSpans(spans = someSpans) { // Действие в builder
}

И последняя функция — buildSpannedString, которая позволяет составить строку, используя вышеперечисленные функции расширения:

textView.text = buildSpannedString { bold { append("hitherejoe") } }

В пакете .net у нас есть одна функция, позволяющая с лёгкостью конвертировать строку в URI. То, что нужно!

val uri = urlString.toUri()

Пакет Graphics в KTX вышел довольно массивным, зато он даёт нам возможность легко реализовывать все визуальные тонкости приложения.
В первую очередь хотелось бы отметить функции, конвертирующие Bitmap (и не только) в следующие типы:

val adaptiveIcon = bitmap.toAdaptiveIcon()
val drawable = bitmap.toDrawable(resources)
val icon = bitmap.toIcon()
val drawable = someInt.toDrawable()
val icon = someByteArray.toIcon()
val icon = someUri.toIcon()
val colorDrawable = someColor.toDrawable()
val bitmap = drawable.toBitmap(width = someWidth, height = someHeight, config = bitMapConfig)

Далее рассмотрим ключевые операции для работы с Bitmap:

val bitmap = someBitmap.applyCanvas(block = { })
val colorInt = someBitmap.get(x, y)
val bitmap = someBitmap.scale(width, height, filter = true)
someBitmap.set(x, y, color)

И работа с Canvas стала намного легче:

canvas.withRotation(degrees, pivotX, pivotY) { // Обработка }
canvas.withSave { // Обработка }
canvas.withScale(x, y, pivotX, pivotY) { // Обработка }
canvas.withSkew(x, y) { // Обработка }
canvas.withTranslation(x, y) { // Обработка }

Также появилось несколько нововведений для Color:

// Получение значений
val (r, g, b, a) = color
// Смешивание цветов
val color += someColor

Функция plus() действительно крута и позволяет нам смешивать два цвета и получить в результате смешанный Color!
Кроме этого, стало проще работать с матрицами. Теперь можно перемножить две матрицы и в результате получить один объект Matrix:

// Умножение
val resultMatrix = matrix * someOtherMatrix
val values = matrix.values()

А ещё мы можем работать с Picture через функцию record, используя блок параметров, чтобы выполнить соответствующие действия:

val resultField = picture.record(width = someWidth, height = someHeight) { // Какие-то действия над Canvas
}

Если мы хотим поменять границы drawable, то можем просто вызвать функцию updateBounds и передать ей размеры в качестве параметров:

drawable.updateBounds(left = 16, top = 16, right = 16, bottom = 16)

Нужно произвести трансформацию на Shader? Без проблем!

shader.transform { // Трансформация }

Появилось несколько функций расширения для работы с классом PorterDuff:

val porterDuffColorFilter = mode.toColorFilter(someColor)
val porterDuffXfermode = mode.toXfermode()

Работая с классом Region, теперь мы можем использовать эти функции:

// Возвращает объединение someRegion с Rect
val region = someRegion and someRect
// Возвращает объединение someRegion с Region
val region = someRegion and someRegion
// Возвращает разницу someRegion и Rect
val region = someRegion - someRect
// Возвращает разницу someRegion и другим Region
val region = someRegion - someRegion
// Возвращает пересечение someRegion и Rect
val region = someRegion or someRect
// Возвращает пересечение someRegion и другим Region
val region = someRegion or someRegion
// Возвращает объединение someRegion с Rect
val region = someRegion + someRect
// Возвращает объединение someRegion с Region
val region = someRegion + someRegion
// Возвращает объединение без пересечения someRegion и Rect
val region = someRegion xor someRect
// Возвращает объединение без пересечения someRegion и другим Region
val region = someRegion xor someRegion
val boolean = someRegion.contains(somePoint)
someRegion.forEach { doSomethingWithEachRect(it) }
val iterator = someRegion.iterator()
// Возвращает инвертированный someRegion как новый Region
val region = -someRegion

Классу PointF также добавили некоторые функции для упрощения:

val (x, y) = somePointF
val pointF = somePointF - someOtherPointF
val pointF = somePointF - someFloat
val pointF = somePointF + somePointF
val pointF = somePointF + someFloat
val point = somePointF.toPoint()
val pointF = -somePointF

Тоже самое доступно для класса Point:

val (x, y) = somePoint
val point = somePoint - somePoint
val point = somePoint - someFloat
val point = somePoint +somePoint
val point = somePoint + someFloat
val point = somePoint.toPointF()
val point = -somePoint

И для класса Rect тоже:

val rect = someRect and anotherRect
val (left, top, right, bottom) = someRect
someRect.contains(somePoint)
val region = someRect - anotherRect
val rect = someRect - someInt
val rect = someRect - somePoint
val rect = someRect or someRect
val rect = someRect + someRect
val rect = someRect + someInt
val rect = someRect + somePoint
val rectF = someRect.toRectF()
val region = someRect.toRegion()
val region = someRect xor someRect

Вы не удивитесь, но для RectF они также доступны:

val rectF = someRectF and anotherRectF
val (left, top, right, bottom) = somerectF
someRectF.contains(somePoint)
val region = someRectF - anotherRectF
val rectF = someRectF - someInt
val rectF = someRectF - somePoint
val rectF = someRectF or someRect
val rectF = someRectF + someRect
val rectF = someRectF + someInt
val rectF = someRectF + somePoint
val rect = someRectF.toRect()
val region = someRectF.toRegion()
val reactF = someRectF.transform(someMatrix)
val region = someRectF xor someRect

При работе с классом Path мы можем использовать следующие варианты:

val path = somePath and anotherPath
val path = somePath.flatten(error = 0.5f)
val path = somePath - anotherPath
val path = somePath or anotherPath
val path = somePath + anotherPath
val path = somePath xor anotherPath

Велика вероятность, что при работе с графикой мы будем работать с типами данных Int и Long. Тип Int предлагает нам следующие функции в KTX:

val alpha = int.alpha
val blue = int.blue
val green = int.green
val red = int.red
val luminance = int.luminance
val (alphaComp, redComp, greenComp, blueComp) = someInt
val color = someInt.toColor()
val color = someInt.toColorLong()

С другой стороны, тип Long содержит немного больше функций:

val alpha = long.alpha
val blue = long.blue
val green = long.green
val red = long.red
val luminance = long.luminance
val (alphaComp, redComp, greenComp, blueComp) = someLong
val color = someLong.toColor()
val color = someLong.toColorInt()
long.isSrgb
long.isWideGamut
long.colorSpace

Итак, дойдя до класса Transition, мы видим, что здесь можно использовать функции расширения, аналогичные animation listeners:

transition.addListener { doSomethingWithTransition(it) }
transition.addListener( onEnd = {}, onStart = {}, onCancel = {}, onResume = {}, onPause = {}
)

Но есть небольшое отличие в синтаксисе метода для отдельных колбэков:

transition.doOnCancel { }
transition.doOnEnd { }
transition.doOnPause { }
transition.doOnResume { }
transition.doOnStart { }

Аналогичные функции были добавлены также и для класса View. Установка колбэков предельно понятна:

view.doOnLayout { }
view.doOnNextLayout { }
view.doOnPreDraw { }

Метод postDelayed теперь доступен в качестве функции:

view.postDelayed(delayInMillis = 200) { // Какое-то действие }

То же самое и с методом postOnAnimationDelayed:

view.postOnAnimationDelayed(delayInMillis = 200) { // Какое-то действие }

Обновление паддингов для View теперь намного легче и понятнее, для этого нам были предоставлены несколько функций:

view.setPadding(16)
view.updatePadding(left = 16, right = 16, top = 16, bottom = 16)
view.updatePaddingRelative( start = 16, end = 16, top = 16, bottom = 16)

Если вам нужно конвертировать View в Bitmap, то теперь это можно сделать одной строкой кода!

val bitmap = view.toBitmap(config = bitmapConfig)

Несколько достаточно крутых функций расширения были добавлены и для ViewGroup. Думаю, вам понравится! Например, проверка, содержит ли ViewGroup конкретную View:

val doesContain = viewGroup.contains(view)

Цикл по child ViewGroup (где it представляет собой child):

viewGroup.forEach { doSomethingWithChild(it) }
viewGroup.forEachIndexed { index, view -> doSomethingWithChild(index, view) }

Доступ к child конкретной позиции в стиле Kotlin:

val view = viewGroup[0]

Получение экземпляра MutableIterator:

val viewGroupIterator = viewGroup.iterator()

И несколько других операций с ViewGroup:

viewGroup.isEmpty()
viewGroup.isNotEmpty()
viewGroup.size
// Удаление view из данной viewgroup
viewGroup -= view
// Добавление view в данную viewgroup
viewGroup += view

Так же, как и паддинги для View, мы можем добавлять margins для LayoutParams с помощью следующих функций:

params.setMargins(16)
params.updateMargins(left = 16, right = 16, top = 16, bottom = 16)
params.updateMarginsRelative( start = 16, end = 16, top = 16, bottom = 16)

Заключение

Как мы видим, KTX предлагает нам мощные инструменты для использования Kotlin в разработке Android-приложений. Я очень рад возможности использовать их в своих проектах и с нетерпением жду, что же будет ещё добавлено в ближайшее время.


x

Ещё Hi-Tech Интересное!

Нечеткая логика против ПИД. Скрещиваем ежа и ужа. Авиадвигатель и алгоритмы управления АЭС

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

Путь интроверта в карьере и бизнесе

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