Kotlin перестал быть «языком для Android» и стал полноценным инструментом для JVM-бэкенда, серверных сервисов, обработки данных и кроссплатформенных приложений. Он совместим с Java-библиотеками, но даёт краткий синтаксис, null-безопасность на уровне типов и корутины вместо ручного управления потоками. Разберём, как поставить инструменты, собрать проект Gradle и писать код, который не разваливается на первой же null-ссылке.

Что Kotlin даёт на практике

  • совместимость с Java: любой jar из Maven Central подключается без обёрток;
  • null-safety: компилятор не даст разыменовать возможный null;
  • корутины и Flow вместо колбэков и пулов потоков;
  • data-классы, sealed-иерархии и исчерпывающие when;
  • единый язык для Android, сервера и Kotlin Multiplatform.

Компилятор K2, ставший основным в Kotlin 2.x, заметно ускорил сборку и улучшил вывод ошибок. Для новых проектов имеет смысл сразу брать актуальную ветку 2.x и Java 21 LTS в качестве целевой платформы.

Установка и первый запуск

Проще всего поставить Kotlin через SDKMAN, который умеет переключать версии. Подойдёт и snap-пакет, если он есть в вашем дистрибутиве. Gradle ставится тем же способом.

sdk install kotlin
sdk install gradle
kotlinc -version
kotlinc hello.kt -include-runtime -d hello.jar
java -jar hello.jar

Минимальная программа выглядит так. Обратите внимание на readlnOrNull: функция возвращает null, если ввод закрыт, и это честно видно в типе.

fun main() {
    val name = readlnOrNull()?.trim()?.takeIf { it.isNotEmpty() } ?: "мир"
    println("Привет, $name!")
}

Для реального проекта компилятор вручную не нужен: сборкой занимается Gradle. Инициализируйте приложение с Kotlin DSL и сразу получите рабочую структуру каталогов.

gradle init --type kotlin-application
./gradlew build
./gradlew run

Синтаксис, который экономит время

В Kotlin val означает неизменяемую ссылку, var — изменяемую. По умолчанию выбирайте val: так проще рассуждать о состоянии. Nullable-типы помечаются вопросительным знаком, а операторы ?. и ?: заставляют явно обработать отсутствие значения.

data class User(val id: Long, val email: String, val nickname: String?)

fun displayName(user: User): String =
    user.nickname?.trim()?.takeIf { it.isNotEmpty() }
        ?: user.email.substringBefore('@')

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

sealed interface UploadResult {
    data class Success(val url: String) : UploadResult
    data class Failure(val reason: String, val retryable: Boolean) : UploadResult
}

fun describe(result: UploadResult): String = when (result) {
    is UploadResult.Success -> "Готово: ${result.url}"
    is UploadResult.Failure ->
        if (result.retryable) "Повторим позже" else "Ошибка: ${result.reason}"
}

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

fun List<Int>.median(): Double {
    val sorted = sorted()
    val middle = sorted.size / 2
    return if (sorted.size % 2 == 0) {
        (sorted[middle - 1] + sorted[middle]) / 2.0
    } else {
        sorted[middle].toDouble()
    }
}

Корутины без блокировки потоков

Корутина — это легковесная задача, которая приостанавливается на suspend-вызове и не занимает поток. Для параллельных запросов используйте async и awaitAll, а для потоков данных — Flow.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

suspend fun fetchPrice(id: Long): Double = withContext(Dispatchers.IO) {
    delay(100)
    199.90 + id
}

fun main() = runBlocking {
    val ids = listOf(1L, 2L, 3L)
    val total = ids.map { async { fetchPrice(it) } }.awaitAll().sum()
    println("Сумма: $total")
}

Flow нужен, когда результатов много и они приходят во времени. Оператор flowOn переносит вычисления на IO-диспетчер, не блокируя вызывающий код.

fun prices(): Flow<Double> = flow {
    repeat(3) { emit(fetchPrice(it.toLong())) }
}.flowOn(Dispatchers.IO)

Не смешивайте runBlocking с серверным кодом: он блокирует поток. В приложении на Ktor или Spring используйте suspend-функции и структурную конкурентность, чтобы отмена родительской задачи отменяла дочерние.

Gradle и структура проекта

Современный build.gradle.kts читается почти как обычный код. Зафиксируйте версию Kotlin, toolchain и зависимости, чтобы сборка была воспроизводимой.

plugins {
    kotlin("jvm") version "2.2.0"
    application
}

repositories { mavenCentral() }

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
    testImplementation(kotlin("test"))
}

kotlin { jvmToolchain(21) }

application { mainClass.set("MainKt") }

В больших проектах версии зависимостей выносят в libs.versions.toml, а для аннотационных процессоров используют KSP вместо kapt. Включите configuration cache и parallel — на многомодульных сборках это экономит минуты.

./gradlew build --configuration-cache
./gradlew test
./gradlew dependencies --configuration runtimeClasspath

Android и Compose

В Android Kotlin — основной язык, а Jetpack Compose описывает интерфейс декларативно. Локальное состояние хранят через remember, а бизнес-состояние — в ViewModel; composable-функции перерисовываются при изменениях.

@Composable
fun Counter() {
    var count by remember { mutableIntStateOf(0) }
    Button(onClick = { count++ }) {
        Text("Нажато: $count")
    }
}

Для Kotlin 2.x плагин компилятора Compose подключается отдельно через org.jetbrains.kotlin.plugin.compose. Сборка и установка отладочного APK на подключённое устройство выполняются стандартными командами.

./gradlew :app:assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk

Не храните бизнес-логику во фрагментах и composable-функциях. Выносите её в ViewModel и обычные классы, которые тестируются на JVM без эмулятора.

Тесты и качество кода

Kotlin-тесты пишутся на том же языке и запускаются через Gradle. Имена в обратных кавычках делают отчёт читаемым, а kotlin.test даёт общий API для JUnit и других фреймворков.

class DiscountTest {
    private fun applyDiscount(price: Double, percent: Int) =
        price * (100 - percent) / 100

    @Test
    fun `скидка применяется к сумме`() {
        assertEquals(90.0, applyDiscount(100.0, 10), 0.001)
    }
}

Добавьте detekt или ktlint в CI, включите предупреждения компилятора как ошибки и не отключайте проверки nullability. Для конкурентного кода используйте kotlinx-coroutines-test и виртуальное время, чтобы тесты не спали по-настоящему.

Чек-лист

  • версия Kotlin и JDK зафиксированы в toolchain;
  • зависимости обновляются осознанно, без гонки за каждой версией;
  • корутины не блокируют потоки и поддерживают отмену;
  • nullability не подавляется оператором !! без крайней необходимости;
  • есть тесты на JVM и отдельные тесты UI для Android;
  • сборка воспроизводится одной командой ./gradlew build.

Итог

Kotlin удобен там, где нужен современный язык без разрыва с Java-экосистемой: на сервере он даёт корутины и лаконичный код, в Android — Compose и единый инструментарий, в мультиплатформе — общую логику. Начните с небольшого сервиса или модуля, закрепите стиль через линтеры и тесты, и только потом переносите на Kotlin крупные части проекта. Главное преимущество языка — не краткость сама по себе, а целые классы ошибок, которые компилятор ловит ещё до запуска.

Насколько публикация полезна?

Нажмите на звезду, чтобы оценить!

Средняя оценка 0 / 5. Количество оценок: 0

Оценок пока нет. Поставьте оценку первым.

Поделиться: Telegram ВКонтакте