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

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

Мы на бирже: https://telega.in/c/KotlinSenior
Download Telegram
В чем отличие 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 используется для определения ограниченного набора значений, которые могут быть произвольными объектами.
Что такое inner (внутренние) и nested (вложенные) классы?

В Kotlin можно объявить один класс внутри другого. Это может быть полезно в тех случаях, когда вам нужно организовать код и логически связать классы между собой. Подобные классы разделяются на внутренние (inner) и вложенные (nested).

1. Внутренние классы (inner classes) имеют доступ к членам внешнего класса, даже если они объявлены как private. Внутренний класс является частью внешнего класса и имеет доступ к его свойствам и методам. В Kotlin внутренний класс объявляется с помощью ключевого слова inner. Например:

class Outer {
private val outerProperty = "Outer Property"

inner class Inner {
fun innerMethod() {
println("Accessing outer property: $outerProperty")
}
}
}

В этом примере Inner является внутренним классом, а Outer является внешним классом. Inner имеет доступ к членам Outer, в том числе к приватным свойствам и методам, таким как outerProperty.

2. Вложенные классы (nested classes) не имеют доступа к членам внешнего класса по умолчанию. Они имеют свои собственные члены, которые могут быть использованы только внутри класса. Например:

class Outer {
private val outerProperty = "Outer Property"

class Nested {
fun nestedMethod() {
println("Accessing nested property")
}
}
}

Здесь Nested является вложенным классом. Он не имеет доступа к свойству outerProperty, но может использовать свои собственные члены, такие как nestedMethod.

3. Ключевое отличие: внутренний (inner) класс — это вложенный (nested) класс, который может обращаться к компонентам внешнего класса.
Строим мосты: подключение зависимостей с Cocoapods в Kotlin Multiplatform Mobile

При создании КММ проекта Android Studio предоставляет разработчику выбор между использованием Regular Framework и Cocoapods Dependency Manager для добавления iOS-специфических библиотек, который может быть крайне неочевидным на первый взгляд, ведь использование Regular Framework кажется затруднительным и не пользуется популярностью в отличие от удобного Cocoapods. В данной статье мы рассмотрим, как интегрировать Cocoapods в разработку, создав небольшое Android приложение.

Читать статью
Какая польза от typealias? Во что он компилируется?

Typealias — это механизм создания синонимов (псевдонимов) для существующих типов. То есть, можно создать новое имя для уже существующего типа данных.

Псевдонимы типов полезны, когда вы хотите сократить длинные имена типов, содержащих обобщения. К примеру, можно упрощать названия типов коллекций:

typealias NodeSet = Set<Network.Node>
typealias FileTable<K> = MutableMap<K, MutableList<File>>

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

К примеру, если в проекте используется много Map<String, String> и вместо этого вы хотите использовать более описательное название, например Properties, вы можете определить новый тип для Map<String, String> с помощью следующего кода:

typealias Properties = Map<String, String>

Теперь вместо использования Map<String, String> можно использовать Properties для обозначения одного и того же типа данных. Таким образом, код становится более читаемым и понятным.

Во что компилируется
typealias?

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

Например, typealias IntPredicate = (Int) -> Boolean при компиляции будет заменено на (Int) -> Boolean, то есть функцию, принимающую значение типа Int и возвращающую значение типа Boolean.

Можно ли использовать
typealias для функциональных типов?

Да, можно использовать typealias для функциональных типов в Kotlin. Например, вы можете создать псевдоним для типа функции, которая принимает два параметра типа Int и возвращает значение типа String, следующим образом:

typealias IntToString = (Int, Int) -> String

Это позволит вам использовать созданный псевдоним вместо полного объявления типа, то есть вместо:

fun processValues(f: (Int, Int) -> String) {
// ...
}

можно использовать:

fun processValues(f: IntToString) {
// ...
}

Как и в случае с другими typealias, компилятор Kotlin просто заменяет псевдоним на соответствующий тип при компиляции кода.
Что такое функции высшего порядка (higher-order functions), лямбда-выражения (lambda-expressions), анонимные функции (anonymous functions), указатели на методы (method references, bound callable references)?

Функции высшего порядка - это функции, которые принимают функцию в качестве аргумента и/или возвращает функцию в качестве результата

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

Анонимные функции - альтернативный синтаксис лямбда выражения с иными правилами для выражения return(можно использовать для создания блока кода с несколькими точками выхода )

Указатели на методы - упрощенный синтаксис создания значения функции, вызывающего ровно один метод или обращающегося к свойству.
{p:Person --> p.age} ==Person::age