JavaRush /Курси /Kotlin SELF /Kotlin 2.x у проєкті: версії, сумісність і дисципліна оно...

Kotlin 2.x у проєкті: версії, сумісність і дисципліна оновлень

Kotlin SELF
Рівень 61 , Лекція 3
Відкрита

1. Шари версій у Kotlin‑проєкті

Коли ви чуєте «давайте оновимо Kotlin до 2.x», дуже хочеться уявити одну ручку «Kotlin version». Мовляв, повернули її — і все стало сучасним і гарним. На практиці Kotlin у проєкті живе одразу в кількох шарах: компілятор, Gradle‑плагін, стандартна бібліотека, залежності та IDE. І «зламатися» може будь-який шар. Причому інколи він ламається дуже ввічливо: просто починає сипати попередженнями, які ви проігноруєте, а за пів року вони перетворяться на помилки.

Щоб не плутатися, давайте введемо зрозумілу «карту місцевості»: де яка версія живе й за що відповідає. Найкорисніше — навчитися розрізняти версію мови (який синтаксис і правила доступні), версію компілятора (як код аналізується і на що перетворюється) та версію інструментів збирання (як проєкт узагалі компілюється в CI і на вашій машині).

Таблиця: які версії є в Kotlin‑проєкті

Шар Що це таке «людською мовою» Де зазвичай налаштовується Типові симптоми проблем
Версія Kotlin Gradle Plugin (KGP) Плагін, який додає в Gradle завдання для компіляції Kotlin build.gradle.kts (plugins { kotlin("jvm") version "…" }) Збирання не стартує, падають Gradle‑завдання, зʼявляються дивні помилки конфігурації
Версія компілятора Kotlin Те, що реально компілює код (зокрема K2‑фронтенд у Kotlin 2.x) Приходить разом із KGP Код, який «учора компілювався», сьогодні не компілюється; діагностика змінюється
Language/API version «Правила мови» й «рівень API», з яким ви сумісні compilerOptions { languageVersionapiVersion} Неочікувані заборони/дозволи можливостей; розбіжності очікувань між модулями
Стандартна бібліотека (stdlib) Базові функції й типи Kotlin Зазвичай підтягується автоматично через KGP, але інколи її фіксують окремою залежністю Конфлікти версій, NoSuchMethodError під час запуску (рідко, але трапляється)
Плагіни та кодогенерація (KSP та ін.) Інструменти, які беруть участь у компіляції Gradle‑плагіни + залежності Генерація перестала працювати, «несумісна версія», падіння на етапі збирання
IDE + Kotlin plugin в IDE Те, що підсвічує код і запускає збирання «з кнопки» IntelliJ IDEA «В IDE все червоне, але Gradle збирає» — або навпаки

Ця таблиця — як аптечка: може виглядати нудно, але в момент болю раптом стає улюбленою.

2. K2, languageVersion і сумісність оновлень

Що таке K2 і чому після оновлення код «червоніє»

Kotlin 2.x — це не лише нові можливості мови, а й великий внутрішній зсув: зʼявився новий фронтенд компілятора (його часто називають K2). Якщо дуже спростити, фронтенд — це частина компілятора, яка читає ваш код, «розуміє», що ви написали, будує внутрішнє представлення та вирішує: «ось так можна, а ось так — ні». Коли фронтенд змінюється, змінюються й діагностики, і трактування деяких «кутових» випадків. Інколи ваш код не став гіршим — просто компілятор став суворішим і передбачуванішим.

Важлива думка для початківця (і для втомленого досвідченого — теж): якщо після оновлення Kotlin у вас зʼявилися нові помилки, це не обовʼязково означає «все зламали». Іноді компілятор просто перестав терпіти те, що раніше мовчки «ковтав».

Приклад: open val та ініціалізація в init

Ось показовий приклад із міграції на Kotlin 2.0: раніше деякі варіанти відкладеної ініціалізації «проходили», а тепер — ні. У Kotlin 2.0 open‑властивості з backing field мають отримувати ініціалізацію одразу. Інакше компілятор свариться.

open class Base {
    open val a: Int
    open var b: Int

    init {
        this.a = 1 // У Kotlin 2.0: помилка — open val має мати initializer
        this.b = 1 // І це завжди було помилкою для open var
    }
}

class Derived : Base() {
    override val a: Int = 2
    override var b: Int = 2
}

Чому це важливо саме в лекції про «версії»? Тому що це класичний сценарій: ви оновили Kotlin — і раптом «зламався» код, якого давно не торкалися. Насправді просто змінилося трактування правила, і проєкт тепер вимагає явніших гарантій.

Як проєкту задати правила мови через languageVersion

Коли проєкт стає більшим за одну папку «з уроками», ви починаєте жити в реальності, де частину коду вже написано «як звикли», частину — «як зараз заведено», а частину — «як підказала IDE о 2‑й ночі». У цей момент особливо важливо вміти керувати правилами компіляції. Іноді ви хочете оновити Gradle‑плагін та інструменти, але тимчасово залишити старі правила мови, щоб міграція пройшла мʼякше. Або навпаки: увімкнути новіші правила там, де вже все готово.

Ключова ідея: версія Kotlin‑плагіна й версія правил мови — не одне й те саме. Так, найчастіше вони рухаються поруч. Але інколи корисно тимчасово «зафіксувати» languageVersion.

Приклад: тимчасово увімкнути правила 1.9

В офіційній настанові з міграції є прямий рецепт: щоб використовувати попередній компілятор у Kotlin 2.0.0+, можна виставити languageVersion у 1.9 або передати -language-version 1.9.

Сенс цього кроку не в тому, щоб «сидіти на 1.9 вічно». Сенс — розділити проблему на частини: спочатку оновити інструменти (Gradle, плагіни), а потім увімкнути нові правила мови й розібрати помилки.

Нижче — приклад, як це може виглядати в build.gradle.kts (спрощено: ідея важливіша за точне місце у файлі):

import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

tasks.withType<KotlinCompile>().configureEach {
    compilerOptions {
        languageVersion.set(KotlinVersion.KOTLIN_1_9)
        apiVersion.set(KotlinVersion.KOTLIN_1_9)
    }
}

Такий перемикач — як режим «тимчасової запаски». Не дуже швидко, зате допомагає доїхати до сервісу, а не лагодити колесо посеред траси.

Чому Kotlin 2.x часто тягне за собою «потяг» залежностей

Є два типи оновлень. Перший — «поміняв цифру в одному місці, і все зібралося». Другий — «поміняв цифру — і раптом дізнався нові слова: ABI, frontend, binary compatibility, plugin API». Kotlin 2.x частіше належить до другого типу, особливо якщо в проєкті є KSP, kotlinx.serialization або інші інструменти, завʼязані на компіляцію.

Найважливіша звичка, яку варто виробити: якщо залежність бере участь у компіляції (а не лише в середовищі виконання), вона чутливіша до версії Kotlin. Це стосується і кодогенерації, і компіляторних плагінів, і навіть того, як IDE аналізує проєкт.

Схема залежностей версій

flowchart TD
    A["Версія Kotlin Gradle Plugin (KGP)"] --> B["Версія компілятора Kotlin (K2/K1)"]
    A --> C["Завдання Gradle: compileKotlin, test, etc."]
    B --> D["Правила мови (languageVersion/apiVersion)"]
    B --> E["Інструменти компіляції: KSP/плагіни"]
    E --> F["Згенеровані .kt файли"]
    F --> C

Ідея проста: якщо ви оновили Kotlin, але забули, що у вас є KSP, який генерує код, то ви оновили «двигун», але залишили старий «карбюратор». Машина (збирання) може почати кашляти.

Практичний принцип сумісності

Якщо сформулювати максимально по‑людськи, правило таке: оновлюйте все, що повʼязано з компіляцією, узгоджено. Якщо ви оновлюєте Kotlin, подивіться на:

  • Gradle‑плагін Kotlin (він же приносить компілятор);
  • KSP (якщо є);
  • плагіни серіалізації та інші compile‑time інструменти (якщо є);
  • і лише потім — звичайні бібліотеки.

Тут важливо не «знати всі версії напамʼять», а мати дисципліну: оновили один шар — перевірили збирання, зафіксували результат — і рухаєтеся далі.

3. Дисципліна оновлень і попередження компілятора

Покроковий сценарій оновлення без «все горить»

У якийсь момент ви почнете оновлювати Kotlin не тому, що «хочеться чогось новенького», а тому, що проєкту потрібен патч безпеки, або бібліотека перестала підтримувати стару версію, або колега надіслав PR зі словами «у мене локально все працює» (давнє закляття хаосу). І тут виграють не ті, хто швидше змінює цифри, а ті, хто робить оновлення передбачуваними.

Далі буде не «ідеальний процес на 40 сторінок», а життєвий варіант — для невеликого навчального або pet‑проєкту. І, що приємно, він масштабується: ті самі ідеї працюють і у великих командах.

Спочатку ви створюєте окрему гілку. Це банально, але саме в цей момент ви «купуєте» собі право на помилку. Потім оновлюєте Kotlin Gradle plugin (зазвичай це рядок версії в plugins {}). Після цього одразу запускаєте збирання через Gradle (не лише з IDE). Річ у тім, що IDE інколи живе «у своєму світі», а Gradle — це світ, у якому живе CI.

Якщо збирання впало, ви не біжите лагодити все підряд. Ви дивитеся, що саме сталося: це помилка конфігурації Gradle (не зібрався скрипт збирання), помилка компіляції Kotlin (не компілюється код) чи помилка тестів (код компілюється, але поведінка змінилася). Це три різні типи проблем — і їх корисно розділяти хоча б у себе в голові.

Далі ви вмикаєте нові правила мови не одразу, а свідомо. Якщо під час переходу на Kotlin 2.x проєкт у режимі K2 одразу починає сваритися на багато місць, ви можете тимчасово виставити languageVersion на 1.9, зібрати проєкт, а потім повернути 2.x і виправляти вже «чисті» проблеми мови, а не одночасно мову + збирання. Сам факт можливості відкату описано в настанові з міграції.

І нарешті — фіксуйте зміни маленькими комітами. Це не бюрократія, а допомога собі в майбутньому: «оновили Kotlin», «полагодили X», «полагодили Y». Так відкат і аналіз стають значно простішими.

Чому попередження в Kotlin 2.x краще не «глушити»

У компілятора є два режими спілкування з вами: «все добре» і «я вас попереджав». Попередження, особливо deprecation‑попередження, часто сприймаються як шум: заважають, підкреслюють код, псують настрій. Але насправді це один із найдешевших способів завчасно дізнатися про майбутні проблеми.

Kotlin і його екосистема регулярно переводять попередження в помилки в міру розвитку мови. Тому звичка «ой, та гаразд, потім» інколи перетворюється на «чому CI червоний, якщо ми нічого не змінювали?». Відповідь зазвичай така: ви не змінювали код, але світ довкола (версії) змінив правила.

Практична стратегія тут звучить нудно, але працює. Якщо попередження можна виправити за 5–10 хвилин без втрати сенсу — виправляйте. Якщо не можна, тоді хоча б зробіть це рішення свідомим: залиште коментар, заведіть задачу або застосуйте придушення (@Suppress(...)) максимально локально. Інакше за пів року ви дивитиметеся на придушення як археолог: «хто це зробив і чому тут поховано мамонта?».

4. Міграція свого API через @Deprecated

Коли ви оновлюєте Kotlin, майже завжди спливає другий шар роботи: ваш власний код теж розвивається. Ви перейменовуєте функції, змінюєте контракти, прибираєте «милиці», поліпшуєте дизайн API. І тут легко зробити боляче користувачам вашого коду (або самому собі за місяць): ви перейменували метод — і весь застосунок розвалився.

Kotlin дає дуже зручний механізм мʼякої міграції: @Deprecated. Це буквально дорожній знак «тут ремонт — обʼїжджайте новою вулицею». І найприємніше: IDE вміє підказувати заміну автоматично через ReplaceWith.

Приклад: міграція функції через ReplaceWith

Уявімо, що в нашому навчальному консольному застосунку (умовно назвемо його StudyTracker) була функція formatMoneyOld(...), а ми хочемо замінити її на formatMoney(...), бо стара назва звучить як зізнання у гріхах.

@Deprecated(
    message = "Використовуйте formatMoney(amountCents)",
    replaceWith = ReplaceWith("formatMoney(amountCents)")
)
fun formatMoneyOld(amountCents: Int): String = formatMoney(amountCents)

fun formatMoney(amountCents: Int): String {
    val dollars = amountCents / 100
    val cents = amountCents % 100
    return "$dollars.${cents.toString().padStart(2, '0')}"
}

Тут важливо, що стару функцію ми не видалили миттєво. Ми зробили її тонкою обгорткою, щоб проєкт продовжував збиратися, а IDE допомагала переписати виклики.

Приклад: міграція властивості через делегування

Іноді ви перейменовуєте не функцію, а властивість. Kotlin дозволяє делегувати одну властивість іншій, і це прямо рекомендують як сценарій сумісного перейменування: вводимо нову властивість, стару позначаємо @Deprecated і делегуємо на нову.

class Settings {
    var currencyCode: String = "USD"

    @Deprecated("Використовуйте 'currencyCode' замість цього", ReplaceWith("currencyCode"))
    var currency: String by this::currencyCode
}

fun main() {
    val s = Settings()
    s.currency = "EUR"
    println(s.currencyCode) // EUR
}

Це виглядає майже як магія, але за змістом усе дуже просто: старе імʼя лишається жити як «псевдонім», а дані насправді зберігаються в новому місці.

5. Типові помилки під час оновлення Kotlin до 2.x

Помилка № 1: оновити Kotlin і ще двадцять залежностей «заодно», а потім шукати джерело поломки.
Так робити дуже хочеться, бо «вже як страждати — то на повну програму». Але це робить діагностику майже неможливою: ви не знаєте, зламалося через Kotlin, через Gradle, через плагін чи через бібліотеку. Набагато надійніше оновлювати шарами й фіксувати маленькі кроки.

Помилка № 2: плутати «версію Kotlin» і «версію languageVersion».
Версія Kotlin Gradle plugin приносить компілятор, але правила мови можна тимчасово зафіксувати. У настанові з міграції прямо згадується варіант відкату на languageVersion 1.9 або -language-version 1.9, щоб використовувати попередній компілятор. Це корисно як тимчасовий захід, але важливо памʼятати: це саме тимчасовий міст, а не «нова норма».

Помилка № 3: ігнорувати зміни семантики та правил, які компілятор почав перевіряти суворіше.
У Kotlin 2.x можна раптово отримати помилку там, де раніше код був «сірим». Приклад із open val, який раніше міг ініціалізуватися в init, а тепер вимагає initializer, добре показує природу таких змін. Тут неправильно злитися на компілятор. Правильніше прийняти, що проєкту потрібні явніші гарантії.

Помилка № 4: ламати своє API «різко», без мʼякої міграції.
Видалити стару функцію, перейменувати властивість і сподіватися «ну я ж усе знайду пошуком» — працює до першого пропущеного місця. Kotlin дає чудовий механізм міграції через @Deprecated і навіть через делегування старої властивості на нову. Якщо ви цим не користуєтеся, ви добровільно відмовляєтеся від безплатної допомоги IDE та компілятора.

Помилка № 5: сприймати попередження як сміття й глушити їх глобально.
Попередження — це часто «рання версія помилки з майбутнього». Якщо їх бездумно придушувати, проєкт стає крихким: він компілюється «зараз», але не підказує, що саме треба виправити перед наступним оновленням. Краще або виправляти, або документувати, чому придушення виправдане, і тримати його локально.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ