[Перевод] Что нового в Kotlin 2.1.20-RC3

В этой статье мы разберём некоторые важные улучшения релиза Early Access Preview (EAP). Полный список изменений можно найти в журнале изменений на GitHub.

Итак, рассмотрим некоторые детали релиза:

  • Компилятор Kotlin K2: новый плагин kapt по умолчанию.

  • Kotlin Multiplatform: новый DSL для замены плагина Gradle Application.

  • Kotlin/Native: новая фаза оптимизации инлайнинга.

  • Kotlin/Wasm: пользовательские форматтеры, включенные по умолчанию, и переход на Provider API.

  • Gradle: поддержка Gradle 8.11, совместимость с Isolated Projects и пользовательские варианты публикации.

  • Стандартная библиотека: универсальные атомарные типы, улучшенная поддержка UUID и новая функциональность для отслеживания времени.

  • Компилятор Compose: информация об исходных файлах включена по умолчанию.

Поддержка IDE

Плагины Kotlin, поддерживающие версию 2.1.20-RC3, уже включены в последние версии IntelliJ IDEA и Android Studio. Вам не нужно обновлять плагин Kotlin в IDE. Нужно только изменить версию Kotlin на 2.1.20-RC3 в скриптах сборки.

Подробнее о том, как обновиться до новой версии, смотрите в этом разделе.

Компилятор Kotlin K2: плагин kapt теперь по умолчанию

Начиная с Kotlin 2.1.20-RC3, реализация плагина kapt для K2 компилятора включена по умолчанию для всех проектов.

Команда JetBrains выпустила новую реализацию плагина kapt с K2 компилятором ещё в Kotlin 1.9.20. С тех пор мы продолжили развивать внутреннюю реализацию K2 kapt и сделали его поведение похожим на версию K1, значительно улучшив при этом производительность.

Если при использовании kapt с K2 компилятором возникают проблемы, можно временно вернуться к предыдущей реализации плагина.

Для этого добавьте следующую опцию в файл gradle.properties проекта:

kapt.use.k2=false

О любых проблемах пишите в трекер ошибок jetbrains.

Kotlin Multiplatform: новый DSL для замены плагина Gradle Application

Начиная с Gradle 8.7, плагин Application больше не совместим с плагином Kotlin Multiplatform Gradle. В Kotlin 2.1.20-RC3 введён экспериментальный DSL, который предоставляет аналогичную функциональность. Новый блок executable {} настраивает задачи выполнения и дистрибуции Gradle для целей JVM.

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

@OptIn(ExperimentalKotlinGradlePluginApi::class)

Затем добавьте новый блок executable {}. Например:

kotlin {
    jvm {
        @OptIn(ExperimentalKotlinGradlePluginApi::class)
        binaries {
            // Конфигурирует задачу JavaExec с именем "runJvm" и Gradle-распределение для компиляции "main" в этой цели
            executable {
                mainClass.set("foo.MainKt")
            }

            // Конфигурирует задачу JavaExec с именем "runJvmAnother" и Gradle-распределение для компиляции "main"
            executable(KotlinCompilation.MAIN_COMPILATION_NAME, "another") {
                // Устанавливает другой класс
                mainClass.set("foo.MainAnotherKt")
            }

            // Конфигурирует задачу JavaExec с именем "runJvmTest" и Gradle-распределение для компиляции "test"
            executable(KotlinCompilation.TEST_COMPILATION_NAME) {
                mainClass.set("foo.MainTestKt")
            }

            // Конфигурирует задачу JavaExec с именем "runJvmTestAnother" и Gradle-распределение для компиляции "test"
            executable(KotlinCompilation.TEST_COMPILATION_NAME, "another") {
                mainClass.set("foo.MainAnotherTestKt")
            }
        }
    }
}

В этом примере плагин Distribution от Gradle применяется на первом блоке executable {}.

О любых проблемах сообщайте в трекер ошибок JetBrains или напишите в публичный канал Slack.

Kotlin/Native: новая оптимизация инлайнинга

Kotlin 2.1.20-RC3 вводит новый этап оптимизации инлайнинга, который выполняется до фазы генерации кода.

Новый этап инлайнинга в компиляторе Kotlin/Native должен быть быстрее стандартного механизма инлайнинга LLVM и улучшить производительность сгенерированного кода.

Этот этап инлайнинга пока является экспериментальным. Чтобы попробовать его в деле, используйте следующую опцию компилятора:

-Xbinary=preCodegenInlineThreshold=40

Наши эксперименты показывают, что 40 — это хорошее компромиссное значение для оптимизации. Согласно нашим бенчмаркам, это приводит к общему улучшению производительности на 9,5%. Конечно, можно попробовать другие значения.

Если вы заметили увеличение размера бинарного файла или времени компиляции, сообщите об этом в YouTrack.

Kotlin/Wasm

Пользовательские форматтеры включены по умолчанию

Ранее нужно было вручную настраивать пользовательские форматтеры для улучшения отладки в веб-браузерах при работе с кодом Kotlin/Wasm.

В этом релизе пользовательские форматтеры включены по умолчанию в сборках для разработки, и дополнительная настройка Gradle не требуется.

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

В Chrome DevTools это доступно через Настройки | Предпочтения | Консоль:

c9ba5797bfad66eaeab3fab12aea05b7.jpg

В Firefox DevTools это доступно через Настройки | Расширенные настройки:

7a3b60e0b029c810737e1712b0aa4d93.png

Это изменение в первую очередь влияет на сборки для разработки. Если у вас есть особые требования для сборок для продакшн-среды, необходимо настроить конфигурацию Gradle. Добавьте следующую опцию компилятора в блок wasmJs {}:

// build.gradle.kts
kotlin {
    wasmJs {
        // ...

        compilerOptions {
            freeCompilerArgs.add("-Xwasm-debugger-custom-formatters")
        }
    }
}

Миграция на Provider API для свойств Kotlin/Wasm и Kotlin/JS

Ранее свойства в расширениях Kotlin/Wasm и Kotlin/JS были изменяемыми (var) и устанавливались непосредственно в скриптах сборки:

the().version = "2.0.0"

Теперь свойства доступны через Provider API, и для установки значений необходимо использовать функцию .set():

the().version.set("2.0.0")

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

С этим изменением прямое присваивание свойств больше не используется в пользу классов *EnvSpec, таких как NodeJsEnvSpec и YarnRootEnvSpec.

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

Устаревшая задача

Замена

wasmJsRun

wasmJsBrowserDevelopmentRun

wasmJsBrowserRun

wasmJsBrowserDevelopmentRun

wasmJsNodeRun

wasmJsNodeDevelopmentRun

wasmJsBrowserWebpack

wasmJsBrowserProductionWebpack или wasmJsBrowserDistribution

jsRun

jsBrowserDevelopmentRun

jsBrowserRun

jsBrowserDevelopmentRun

jsNodeRun

jsNodeDevelopmentRun

jsBrowserWebpack

jsBrowserProductionWebpack или jsBrowserDistribution

Если вы используете только Kotlin/JS или Kotlin/Wasm в скриптах сборки, дополнительные действия не требуются, так как Gradle автоматически обрабатывает назначения.

Однако если вы поддерживаете плагин на основе Kotlin Gradle и ваш плагин не использует kotlin-dsl, нужно обновить назначения свойств, чтобы использовать функцию .set().

Gradle

Поддержка версии 8.11

Kotlin 2.1.20-RC3 теперь совместим с последней стабильной версией Gradle 8.11 и поддерживает её функции.

Плагин Kotlin Gradle совместим с функцией Isolated Projects Gradle

Эта функция в данный момент находится в предварительной альфа-стадии в Gradle. Используйте её только с Gradle версии 8.10 или выше и исключительно для целей оценки.

С версии Kotlin 2.1.0 вы можете предварительно протестировать функцию Isolated Projects в своих проектах.

Ранее вам нужно было настроить плагин Kotlin Gradle, чтобы проект стал совместим с функцией Isolated Projects, прежде чем её можно было использовать. В Kotlin 2.1.20-RC3 этот дополнительный шаг больше не требуется.

Теперь для включения функции Isolated Projects достаточно установить системное свойство.

Функция Isolated Projects поддерживается в плагинах Kotlin Gradle как для мультиплатформенных проектов, так и для проектов, которые содержат только цели JVM или Android.

Для мультиплатформенных проектов, если после обновления вы столкнулись с проблемами в процессе сборки, вы можете отказаться от нового поведения плагина Kotlin Gradle, установив:

kotlin.kmp.isolated-projects.support=disable

Однако, если вы используете это свойство Gradle в своём мультиплатформенном проекте, вы не сможете использовать функцию Isolated Projects.

Отзыв по этой функции можете оставить в YouTrack.

Поддержка добавления пользовательских вариантов публикации Gradle

Kotlin 2.1.20-RC3 вводит поддержку добавления пользовательских вариантов публикации Gradle. Эта функция доступна для мультиплатформенных проектов и проектов, ориентированных на JVM.

Она не позволяет изменять существующие варианты Gradle и является экспериментальной. Для её использования примените аннотацию @OptIn(ExperimentalKotlinGradlePluginApi::class).

Чтобы добавить пользовательский вариант публикации Gradle, вызовите функцию adhocSoftwareComponent(), которая возвращает экземпляр AdhocComponentWithVariants, который можно настроить в Kotlin DSL:

plugins {
    // Поддерживаются только JVM и Multiplatform
    kotlin("jvm")
    // или
    kotlin("multiplatform")
}

kotlin {
    @OptIn(ExperimentalKotlinGradlePluginApi::class)
    publishing {
        // Возвращает экземпляр AdhocSoftwareComponent
        adhocSoftwareComponent()
        // Альтернативно, вы можете настроить AdhocSoftwareComponent в блоке DSL следующим образом
        adhocSoftwareComponent {
            // Добавьте ваши собственные варианты здесь, используя API AdhocSoftwareComponent
        }
    }
}

Стандартная библиотека

Универсальные атомарные типы

В Kotlin 2.1.20-RC3 мы вводим универсальные атомарные типы в стандартной библиотеке в пакете kotlin.concurrent.atomics, что позволяет создавать платформонезависимый код для потокобезопасных операций. Это упрощает разработку для проектов Kotlin Multiplatform, устраняя необходимость дублировать логику, зависимую от атомарных типов, в разных исходных наборах.

Пакет kotlin.concurrent.atomics и его элементы являются экспериментальными. Чтобы воспользоваться этой функциональностью, используйте аннотацию @OptIn(ExperimentalAtomicApi::class) или опцию компилятора -opt-in=kotlin.ExperimentalAtomicApi.

Вот пример, который показывает, как можно использовать AtomicInt для безопасного подсчёта обработанных элементов в нескольких потоках:

@OptIn(ExperimentalAtomicApi::class)
suspend fun main() {
    // Инициализирует атомарный счётчик для обработанных элементов
    var processedItems = AtomicInt(0)
    val totalItems = 100
    val items = List(totalItems) { "item$it" }
    // Разбивает элементы на части для обработки несколькими корутинами
    val chunkSize = 20
    val itemChunks = items.chunked(chunkSize)
    coroutineScope {
        for (chunk in itemChunks) {
            launch {
                for (item in chunk) {
                    println("Обрабатывается $item в потоке ${Thread.currentThread()}")
                    processedItems += 1 // Увеличивает счётчик атомарно
                }
            }
        }
    }
}

Чтобы обеспечить бесперебойную совместимость атомарных типов Kotlin и Java, API предоставляет функции расширения .asJavaAtomic() и .asKotlinAtomic(). На JVM атомарные типы Kotlin и Java — это одни и те же типы во время выполнения, поэтому атомарные типы Java можно преобразовывать в атомарные типы Kotlin и наоборот без дополнительных расходов.

Вот пример того, как могут работать атомарные типы Kotlin и Java вместе:

@OptIn(ExperimentalAtomicApi::class)
fun main() {
    // Преобразует Kotlin AtomicInt в Java AtomicInteger
    val kotlinAtomic = AtomicInt(42)
    val javaAtomic: AtomicInteger = kotlinAtomic.asJavaAtomic()
    println("Значение Java atomic: ${javaAtomic.get()}")
    // Значение Java atomic: 42

    // Преобразует Java AtomicInteger обратно в Kotlin AtomicInt
    val kotlinAgain: AtomicInt = javaAtomic.asKotlinAtomic()
    println("Значение Kotlin atomic: ${kotlinAgain.load()}")
    // Значение Kotlin atomic: 42
}

Изменения в разборе, форматировании и сопоставимости UUID

Команда JetBrains продолжает улучшать поддержку UUID, введённую в стандартной библиотеке в версии 2.0.20.

Ранее функция parse() принимала только UUID в формате с шестнадцатеричными числами и дефисами. В Kotlin 2.1.20-RC3 теперь можно использовать parse() для обоих форматов — с дефисами и без них (простой шестнадцатеричный формат).

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

  • parseHexDash() — парсит UUID из формата с шестнадцатеричными числами и дефисами.

  • toHexDashString() — преобразует UUID в String в формате с шестнадцатеричными числами и дефисами (аналогично функции toString()).

Эти функции работают аналогично parseHex() и toHexString(), которые были введены ранее для шестнадцатеричного формата. Явное наименование функций для парсинга и форматирования должно улучшить читаемость кода и общий опыт работы с UUID.

UUID в Kotlin теперь поддерживает сравнение (Comparable). Начиная с Kotlin 2.1.20-RC3, сравнивать и сортировать значения типа Uuid можно напрямую. Это позволяет использовать операторы < и >, стандартные расширения библиотеки для типов, реализующих интерфейс Comparable или их коллекций (например, sorted()), а также передавать UUID в функции или API, которые требуют интерфейс Comparable.

Помните, что поддержка UUID в стандартной библиотеке всё ещё является экспериментальной. Чтобы использовать её, добавьте аннотацию @OptIn(ExperimentalUuidApi::class) или опцию компилятора -opt-in=kotlin.uuid.ExperimentalUuidApi.

@OptIn(ExperimentalUuidApi::class)
fun main() {
    // parse() принимает UUID в обычном шестнадцатеричном формате
    val uuid = Uuid.parse("550e8400e29b41d4a716446655440000")

    // Преобразует его в формат с дефисами и шестнадцатеричными числами
    val hexDashFormat = uuid.toHexDashString()

    // Выводит UUID в формате с дефисами и шестнадцатеричными числами
    println(hexDashFormat)

    // Выводит UUID в порядке возрастания
    println(
        listOf(
            uuid,
            Uuid.parse("780e8400e29b41d4a716446655440005"),
            Uuid.parse("5ab88400e29b41d4a716446655440076")
        ).sorted()
    )
}

Новая функциональность для отслеживания времени 

Начиная с Kotlin 2.1.20-RC3, стандартная библиотека даёт возможность представлять момент времени. Эта функциональность ранее была доступна только в kotlinx-datetime — официальной библиотеке Kotlin.

Интерфейс kotlinx.datetime.Clock был добавлен в стандартную библиотеку как kotlin.time.Clock, а класс kotlinx.datetime.Instant как kotlin.time.Instant. Эти концепции естественным образом интегрируются с пакетом времени в стандартной библиотеке, поскольку они касаются только моментов времени, в отличие от более сложной функциональности для работы с календарём и временными зонами, которая остаётся в kotlinx-datetime.

Instant и Clock полезны, когда нужно точно отслеживать время без учёта временных зон или дат. Например, их можно использовать для логирования событий с метками времени, измерения продолжительности между двумя моментами времени и получения текущего момента для системных процессов.

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

  • .toKotlinInstant() — преобразует значение времени в экземпляр kotlin.time.Instant.

  • .toJavaInstant() — преобразует значение kotlin.time.Instant в значение java.time.Instant.

  • Instant.toJSDate() — преобразует значение kotlin.time.Instant в экземпляр класса JS Date. Это преобразование не является точным, так как JS использует миллисекундную точность для представления дат, в то время как Kotlin поддерживает разрешение до наносекунд.

Новые функции работы с временем в стандартной библиотеке всё ещё являются экспериментальными. Чтобы воспользоваться ими, используйте аннотацию @OptIn(ExperimentalTime::class).

import kotlin.time.*

@OptIn(ExperimentalTime::class)
fun main() {

    // Получает текущий момент времени
    val currentInstant = Clock.System.now()
    println("Текущее время: $currentInstant")

    // Находит разницу между двумя моментами времени
    val pastInstant = Instant.parse("2023-01-01T00:00:00Z")
    val duration = currentInstant - pastInstant

    println("Время, прошедшее с 2023-01-01: $duration")
}

Для получения дополнительной информации о реализации смотрите это предложение KEEP.

Компилятор Compose: информация об исходных файлах включена по умолчанию

Плагин компилятора Compose для Gradle включает информацию о исходниках по умолчанию для всех платформ. Опция includeSourceInformation уже была включена для Android, и это изменение унифицирует поведение плагина между платформами, а также поддерживает новые функции времени выполнения.

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

Критические изменения и устаревшие функции

Для согласования Kotlin Multiplatform с предстоящими изменениями в Gradle мы постепенно выводим функцию withJava(). Наборы исходных файлов для Java теперь создаются по умолчанию.

Как обновиться до Kotlin 2.1.20-RC3

Начиная с версии IntelliJ IDEA 2023.3 и Android Studio Iguana (2023.2.1) Canary 15, плагин Kotlin теперь распространяется как встроенный, включённый в IDE. Это означает, что плагин больше нельзя установить через JetBrains Marketplace. Встроенный плагин поддерживает предстоящие релизы Kotlin EAP.

Чтобы обновиться до новой версии Kotlin EAP, измените версию Kotlin в скриптах сборки.

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

Вот темы, которые стоит посетить:

  • 3 апреля. Оптимизация CI/CD для мобильных тестов на Kotlin: как избавиться от нестабильных тестов и ускорить развертывание? Записаться

  • 10 апреля. Контрактное тестирование в Kotlin QA: как гарантировать, что фронтенд и бэкенд понимают друг друга? Записаться

  • 17 апреля. Применение возможностей Kotlin в UI тестировании: как Kotlin помогает в тестировании пользовательского интерфейса. Записаться

Habrahabr.ru прочитано 13931 раз