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 { languageVersion… apiVersion… } | Неочікувані заборони/дозволи можливостей; розбіжності очікувань між модулями |
| Стандартна бібліотека (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: сприймати попередження як сміття й глушити їх глобально.
Попередження — це часто «рання версія помилки з майбутнього». Якщо їх бездумно придушувати, проєкт стає крихким: він компілюється «зараз», але не підказує, що саме треба виправити перед наступним оновленням. Краще або виправляти, або документувати, чому придушення виправдане, і тримати його локально.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ