Новая схема раздельной компиляции 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 и сломать обращение к .name, вызывая ошибки сборки только на JVM.

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