Kotlin Developer
6.22K subscribers
258 photos
8 videos
353 links
Самый топовый канал по Kotlin

По вопросам сотрудничества и рекламы: @NadikaKir

Мы на бирже: https://telega.in/c/KotlinSenior
Download Telegram
Разница между var, val, const val

1. var — это изменяемая переменная. После инициализации мы можем изменять данные, хранящиеся в переменной.

Переменные val и const val доступны только для чтения — это неизменяемые переменные.

2. val — константа времени выполнения, т.е. значение можно назначить во время выполнения программы.

3. const val — константа времени компиляции, т.к. значения константам присваивается при компиляции (в момент, когда программа компилируется).

В отличие от val, значение const val должно быть известно во время компиляции.

Особенности const val:

• могут получать значение только базовых типов: Int, Double, Float, Long, Short, Byte, Char, String, Boolean.

• объявляются в глобальной области видимости, то есть за пределами функции main() или любой другой функции.

• нет пользовательского геттера.
Как стоит объявлять свои константы в Kotlin — при помощи companion object или вне класса?

На самом деле оба эти подхода приемлемы. Однако, использование companion object может быть излишним: компилятор Kotlin преобразует companion object во вложенный класс. Слишком много кода для простой константы.

Если вам не требуется поведение, специфичное для companion object, объявляйте константы вне класса, так как это будет способствовать более эффективному байт-коду. Да и сам синтаксис объявления констант вне класса более чистый и читабельный.
Как создать анимированные шейдеры в Jetpack Compose

Jetpack Compose — молодой, но бурно развивающийся фреймворк для разработки под Android, который обладает множеством не всегда очевидных фичей. Сегодня я хотел бы описать одну из таких встроенных возможностей: речь идет об использовании OpenGL-шейдеров. Они позволяют делать красивые анимированные интерфейсы.

Читать статью
Свойства, методы get и set

Свойства класса — это переменные, которые хранят состояние объекта класса. Как и любая переменная, свойство может иметь тип, имя и значение.

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

class Person {
var name: String = ""
val age: Int = 0
}

При создании своего класса мы хотим сами управлять его свойствами, контролируя то, какие данные могут быть предоставлены или перезаписаны. С этой целью создаются get и set методы (геттеры и сеттеры). Цель get-метода — вернуть значение, а set-метода — записать полученное значение в свойство класса.

var name: String = ""
get() = field.toUpperCase()
set(value) {
field = "Name: $value"
}

В данном примере свойство name имеет тип String и начальное значение пустой строки. Геттер возвращает значение свойства, преобразованное к верхнему регистру. Сеттер устанавливает значение свойства с добавлением префикса "Name: " перед переданным значением. Слово field используется для обращения к текущему значению свойства.

Если get и set методы не были созданы вручную, то для таких свойств Kotlin незаметно сам их генерирует. При этом для свойства, объявленного с val, генерируется get-метод, а для свойства, объявленного с var — и get, и set методы.
В чем отличие field от property?

В Kotlin свойство (property) — это абстракция над полями (fields), которая позволяет обращаться к значению переменной через методы геттера и сеттера, вместо прямого доступа к полю.

Field — это переменная, которая содержит значение и может быть доступна напрямую или через геттер/сеттер.

Пример определения свойства с геттером и сеттером в классе:

class Person {
var name: String = ""
get() = field.toUpperCase() // возвращает значение поля name в верхнем регистре
set(value) {
field = value.trim() // устанавливает значение поля name без начальных и конечных пробелов
}
}

В данном примере свойство name содержит поле, которое может быть доступно напрямую только внутри класса, и методы геттера и сеттера, которые позволяют получать и изменять значение свойства через специальные методы.
Что такое делегированные свойства (Delegated properties)?

Делегированные свойства (Delegated properties) — это свойства, которые не хранят своё значение напрямую, а делегируют это значение другому объекту, который реализует интерфейс Delegate. При доступе к свойству, его значение запрашивается у делегата, который может выполнить какую-то дополнительную логику, а затем вернуть требуемое значение. Пример:

class Example {
var p: String by Delegate()
}

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

Синтаксис выглядит так: val/var <имя свойства>: <Тип> by <выражение>. Выражение после by — делегат, потому что обращения get(), set() к свойству будут делегированы его методам getValue() и setValue(). Делегат не обязан реализовывать какой-то интерфейс, достаточно, чтобы у него были метод getValue() (и setValue() для var'ов) с определённой сигнатурой.

В Kotlin существуют несколько встроенных делегатов для работы с делегированными свойствами:

• lazy() — позволяет создавать лениво инициализированные свойства

• observable() — позволяет реагировать на изменения свойства

• vetoable() — позволяет отклонять изменения значения свойства на основе заданного условия

• notNull() — гарантирует, что свойство не будет иметь значение null

• map() — позволяет хранить значения свойств в словаре (Map)

Кроме того, в Kotlin можно создавать свои собственные делегаты, реализуя интерфейс ReadOnlyProperty или ReadWriteProperty. Это дает возможность создавать кастомные поведения для свойств, например, кеширование значений или логирование операций чтения/записи.
Блок инициализации (init блок)

Основной конструктор не может в себе содержать какую-либо логику по инициализации свойств (исполняемый код). Он предназначен исключительно для объявления свойств и присвоения им полученных значений. Поэтому вся логика может быть помещена в блок инициализации — блок кода, обязательно выполняемый при создании объекта независимо от того, с помощью какого конструктора этот объект создаётся. Помечается он словом init.

class Person(val name: String, var age: Int) {
var id: Int = 0

// require выдает ошибку с указанным текстом, если условие в левой части false
init {
require(name.isNotBlank(), { "У человека должно быть имя!" })
require(age > -1, { "Возраст не может быть отрицательным." })
}

constructor(name: String, age: Int, id: Int) : this(name, age) {
if (id > 0) this.id = id * 2
}
}

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

class Person(val name: String, var age: Int) {
// сначала вызывается основной конструктор и создаются свойства класса
// далее вызывается первый блок инициализации
init {
...
}

// после первого вызывается второй блок инициализации
    init {
...
}

// и т.д.
}

Блок инициализации может быть добавлен, даже если у класса нет основного конструктора. В этом случае его код будет выполнен раньше кода вторичных конструкторов.
Расскажите о Data классах. Какие преимущества они имеют?

Data класс предназначен исключительно для хранения каких-либо данных.

Основное преимущество: для параметров, переданных в основном конструкторе автоматически будут переопределены методы toString(), equals(), hashCode(), copy().

Также для каждой переменной, объявленной в основном конструкторе, автоматически генерируются функции componentN(), где N — номер позиции переменной в конструкторе.

Благодаря наличию вышеперечисленных функций внутри data класса мы исключаем написание шаблонного кода.
Пару слов о полях и свойствах в Kotlin

Терминология свойств и полей в Kotlin может немного сбивать с толку, потому что технически в Kotlin нет полей. Вы не можете объявить поле. Все — свойства! Однако, во избежании путаницы, я предпочитаю разделять определения полей и свойств на следующей основе — полями являются приватные переменные-члены класса. Это то, для чего выделена память. Свойствами являются публичные или защищенные (protected) функциями геттеры и сеттеры, которые позволяют вам получить доступ к приватным полям.

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

Читать статью
Что такое мульти-декларации (destructuring declarations)?

Мульти-декларации (destructuring declarations или деструктуризирующее присваивание) — это способ извлечения значений из объекта и присвоения их сразу нескольким переменным. В Kotlin этот механизм поддерживается с помощью оператора распаковки (destructuring operator) — componentN(), где N — номер компонента.

При создании data класса Kotlin автоматически создает функции componentN() для каждого свойства класса, где N — номер позиции переменной в конструкторе. Функции componentN() возвращают значения свойств в порядке их объявления в конструкторе. Это позволяет использовать мульти-декларации для распаковки значений свойств и присваивания их отдельным переменным.

Например, если у нас есть data класс Person с двумя свойствами name и age, мы можем использовать мульти-декларации, чтобы извлечь эти свойства и присвоить их двум переменным:

data class Person(val name: String, val age: Int)

val person = Person("Alice", 29)
val (name, age) = person

println(name) // Alice
println(age) // 29

Также можно использовать мульти-декларации в циклах, чтобы итерироваться по спискам объектов и распаковывать значения свойств:

val people = listOf(Person("Alice", 30), Person("Bob", 40))
for ((name, age) in people) {
println("$name is $age years old")
}

// Alice is 30 years old
// Bob is 40 years old

Мульти-декларации также могут быть использованы с массивами и другими коллекциями:

val list = listOf("apple", "banana", "orange")
val (first, second, third) = list

println(first) // apple
println(second) // banana
println(third) // orange
Что делает функция componentN()?

Функция componentN() возвращает значение переменной и позволяет обращаться к свойствам объекта класса по их порядковому номеру. Генерируется автоматически только для data классов.

Также функцию componentN() можно создать самому для класса, который не является data классом.

class Person(val firstName: String, val lastName: String, val age: Int) {
operator fun component1() = firstName
operator fun component2() = lastName
operator fun component3() = age
}

Теперь можно использовать мульти-декларации для класса Person:

val person = Person("John", "Doe", 30)
val (firstName, lastName, age) = person
println("$firstName $lastName is $age years old.")

В данном примере мы определили функции component1(), component2() и component3() как операторы с ключевым словом operator. Они возвращают значения свойств firstName, lastName и age соответственно. После этого мы можем использовать мульти-декларации для разбивки объекта Person на отдельные переменные.
Какие требования должны быть соблюдены для создания data класса?

Класс должен иметь хотя бы одно свойство, объявленное в основном конструкторе.

Все параметры основного конструктора должны быть отмечены val или var (рекомендуется val).

Классы данных не могут быть abstract, open, sealed или inner.
Kotlin вместо bash. Прокачиваем автоматизацию на сервере

Для решения задач автоматизации рутинных процессов для системных администраторов и DevOps чаще всего используются или bash-сценарии или python. Первое решение косвенно используется и в описании Dockerfile, поскольку сценарий исполняемых команд принципиально ничем не отличается от запуска скрипта в какой-либо shell, второй подход чаще ассоциируется с автоматизацией, связанных с взаимодействием с хранилищами данных. Но несправедливо было бы обойти стороной возможность создания исполняемых сценариев на языке Kotlin, которые могут стать полноценной заменой bash-сценариям.

В этой статье мы рассмотрим несколько примеров использования Kotlin Scripting (KTS) для автоматизации в распределенной системе, будем использовать долгоживущие скрипты с ожиданием заданий через RabbitMQ, а также поработаем с файловой системой, внешними сервисами, а также попробуем использовать KTS для сборки Docker-контейнеров
.

Читать статью
Можно ли наследоваться от data класса?

От data класса нельзя наследоваться т.к. он является final классом, но он может наследоваться от других классов.
Почему классы в Kotlin по умолчанию final?

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

В Kotlin рекомендуется использовать композицию вместо наследования для повторного использования кода и расширения функциональности.
Что нужно сделать, чтобы класс можно было наследовать? (open)

По умолчанию, классы в Kotlin объявляются как final, то есть их нельзя наследовать. Если мы всё же попытаемся наследоваться от такого класса, то получим ошибку: “This type is final, so it cannot be inherited from”.

Чтобы класс можно было наследовать, его нужно объявить с модификатором open.

open class Fraction {
...
}

Не только классы, но и функции в Kotlin по умолчанию имеют статус final. Поэтому те функции, которые находятся в родительском классе и которые вы хотите переопределить в дочерних классах, также должны быть отмечены open.

open class Fraction {

open fun toAttack() {
...
}

}

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

open class Fraction {

open val name: String = "default"

open fun toAttack() {
...
}

}

При этом, если в открытом классе будут присутствовать функции и свойства, которые не отмечены словом open, то переопределяться они не будут. Но дочерний класс сможет к ним обращаться.

open class Fraction {

open val name: String = "default"

fun toAttack() {
...
}

}

class Horde : Fraction() {
override val name = "Horde"
}

class SomeClass() {
val horde = Horde()
horde.toAttack()
}
Как можно получить тип класса?

1. Получение типа класса через функцию ::class

Функция ::class возвращает объект KClass, который содержит информацию о типе класса во время выполнения.

class Person(val name: String, val age: Int)

fun main() {
val person = Person("John", 30)
println(person::class) // выводит "class Person"
}

2. Получение типа класса через функцию javaClass

Функция javaClass возвращает объект Class, который содержит информацию о типе класса во время выполнения.

class Person(val name: String, val age: Int)

fun main() {
val person = Person("John", 30)
println(person.javaClass) // выводит "class Person"
}

3. Получение типа класса через функцию ::class.java

Вызов функции ::
class.java на объекте типа KClass возвращает объект Class, который содержит информацию о типе класса во время выполнения.

class Person(val name: String, val age: Int)

fun main() {
val person = Person("John", 30)
println(person::class.java) // выводит "class Person"
}
Осознанная оптимизация Compose

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

Наша команда Ozon Seller также столкнулась с этой проблемой. Мы решили собрать воедино все советы и наработки по написанию оптимизированного Compose-кода. Активное применение этих советов при оптимизации существующих экранов и написании новых существенно улучшило наши метрики: длительность лага по отношению к длительности скролла (hitch rate; чем меньше, тем лучше) экранов со списками упала в среднем с 15-19 % до 5-7 % (на 90-м перцентиле). Все эти советы и наработки мы описали в этой статье. Она будет полезна и начинающим, и опытным разработчикам, в ней подробно описаны оптимизации и механизмы Compose, а также рассказано про слабо задокументированных особенности и исправления ошибок, которые есть в других статьях. Давайте же начнём.

Читать статью
Что такое sealed класс (изолированный)?

Sealed class (изолированный класс) — это класс, который является абстрактным и используется в Kotlin для ограничения классов, которые могут наследоваться от него.

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

Конструктор изолированного класса всегда приватен, и это нельзя изменить.

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

Наследники sealed класса могут быть классами любого типа: data class, объектом, обычным классом, другим sealed классом. Классы, которые расширяют наследников sealed класса могут находиться где угодно.

Изолированные классы абстрактны и могут содержать в себе абстрактные компоненты.

Изолированные классы нельзя инициализировать.

При использовании when, все подклассы, которые не были проверены в конструкции, будут подсвечены IDE.

Не объявляется с ключевым словом inner.

Пример sealed класса:

sealed class Shape {
class Circle(val radius: Double) : Shape()
class Rectangle(val width: Double, val height: Double) : Shape()
class Triangle(val base: Double, val height: Double) : Shape()
}

fun calculateArea(shape: Shape): Double {
return when (shape) {
is Shape.Circle -> Math.PI * shape.radius * shape.radius
is Shape.Rectangle -> shape.width * shape.height
is Shape.Triangle -> 0.5 * shape.base * shape.height
}
}

fun main() {
val circle = Shape.Circle(5.0)
val rectangle = Shape.Rectangle(2.0, 3.0)
val triangle = Shape.Triangle(4.0, 5.0)

println(calculateArea(circle)) // Output: 78.53981633974483
println(calculateArea(rectangle)) // Output: 6.0
println(calculateArea(triangle)) // Output: 10.0
}

В этом примере мы определили sealed class Shape, который содержит три класса: Circle, Rectangle и Triangle. Эти классы наследуются от Shape. Это означает, что мы можем создавать объекты этих классов и использовать их, как объекты типа Shape.

В функции calculateArea мы используем выражение when, чтобы определить тип фигуры и вернуть ее площадь. Таким образом, если мы передадим Shape.Circle в calculateArea, то будет вычислена площадь круга.

В функции main мы создали объекты Circle, Rectangle и Triangle и передали их в calculateArea, чтобы вычислить их площади.
Какая разница между sealed class и enum?

Sealed class и Enum это два разных концепта в Kotlin, хотя их часто используют для ограничения набора возможных значений. Основная разница между ними:

• enum представляет собой конечный список значений, которые объявляются заранее в момент компиляции, и не могут быть расширены или изменены во время выполнения программы

• sealed class позволяет определять ограниченный набор значений, но эти значения могут быть расширены в будущем

В общем, enum class используется для представления конечного списка опций или состояний, тогда как sealed class используется для определения ограниченного набора значений, которые могут быть произвольными объектами.