Нова схема роздільної компіляції Kotlin приводить мультиплатформні збірки у відповідність до IDE
Public- 28 Sep, 2026

Чинний підхід до компіляції Kotlin Multiplatform працює, але може давати несподівану та важкопередбачувану поведінку. Суть проблеми в розбіжності між IDE та компілятором: вони по-різному розв'язують виклики зі спільних наборів вихідних кодів до оголошень із залежностей. У Kotlin 2.5.0-Beta1 JetBrains додали опціональну «роздільну компіляцію», покликану це виправити — привести результат компіляції у відповідність до аналізу в IDE та точніше вказувати на проблемні виклики зі спільних наборів вихідних кодів у код бібліотек. Бонусом схема ще й вмикає інкрементальну компіляцію для спільних наборів вихідних кодів.
Проблема сягає корінням у будову модуля KMP. Модуль зі спільним кодом зазвичай містить кілька наборів вихідних кодів: спільний (commonMain) і набори для конкретних платформ (jvmMain для Kotlin/JVM). Під час компіляції платформного набору вихідних кодів код у jvmMain плюс усі пов'язані спільні набори компілюється проти платформних артефактів залежностей. Через це код у commonMain отримує можливість неявно розв'язуватися до оголошення з jvmMain у залежності — а саме цього IntelliJ IDEA не допускає. Оскільки оголошення jvmMain не потрапляють до метаданих KLIB, IDE показує для таких викликів помилку «unresolved reference», тоді як компілятор мовчки їх приймає, і код збирається без помилок.

Увімкнення роздільної компіляції змушує компілятор поводитися суворіше, узгоджуючи його поведінку з IDE. Опціональний прапорець додається в gradle.properties як kotlin.kmp.separateCompilation=true, і за замовчуванням функція вимкнена, бо вона Experimental. З увімкненою опцією код, який раніше збирався, тепер падає: наприклад, виклик Foo з app/commonMain, коли class Foo оголошений лише в lib/jvmMain, стає помилкою компіляції, а не попередженням лише в IDE. Виправлення — налагодити явний зв'язок через expect/actual: оголосити expect class Foo у lib/commonMain і позначити платформну версію як actual class Foo. Те саме правило діє для спільних тестових наборів commonTest, адже тести «залежать» від основного коду; commonTest, скомпільований проти jvmMain, більше не може дотягнутися до платформних оголошень без відповідного expect у commonMain.
Схема також усуває розбіжності під час вибору перевантаження. Коли в lib/commonMain і lib/jvmMain є кілька перевантажень, IDE і компілятор зараз обирають різні оголошення. Наприклад, при fun foo(x: Any) у commonMain і fun foo(x: String) в jvmMain IDE розв'язує foo("") до загальної перевантаження Any, але компіляція обирає конкретнішу перевантаження String, а в рантаймі друкується «platform». При роздільній компіляції виклик розв'язується до спільного оголошення, і друкується «common», що збігається з поведінкою IDE. Більш хитрий збій з'являється, коли помилка спливає лише в платформному коді: якщо actual-класи додають супертипи, яких немає в їхніх expect-аналогів, виведення типу, що повертається, може на JVM схлопнутись до Any Any .nameспричиняючи помилки збірки лише на JVM.

За окремої компіляції app/commonMain розв'язує виклики лише за оголошеннями з eventsLib/commonMain, тому компілятор виводить правильний тип повернення Event, і компіляція для JVM завершується чисто. JetBrains документують відомі проблеми, які ще виправляються. Головне: щоб окрема компіляція працювала коректно, автори бібліотек зобов'язані публікувати метадані KLIB — це вже поведінка задач публікації плагіна KMP Gradle за замовчуванням, але нова схема робить ці KLIB обов'язковими. Оскільки функція Experimental, очікуйте суворіших перевірок на етапі компіляції та плануйте відповідно адаптувати наявний код.
