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 крупные части проекта. Главное преимущество языка — не краткость сама по себе, а целые классы ошибок, которые компилятор ловит ещё до запуска.