Как работает компиляция в Kotlin Multiplatform
Что проверяют
Понимаете ли вы, что в KMP нет единого «общего бинарника»: общий код компилируется отдельно под каждую платформу своим бэкендом. Следом обычно спрашивают, как устроены expect/actual и откуда iOS-приложение берёт Kotlin-код.
Как отвечать
Начните со структуры проекта. Код раскладывается по source sets: commonMain для общего, androidMain, iosMain и другие для платформенного. Промежуточные наборы вроде appleMain плагин собирает сам по шаблону иерархии по умолчанию.
Дальше сам конвейер:
- фронтенд: компилятор K2 разбирает код и строит промежуточное представление (IR). Общий код проверяется отдельно от платформенного, поэтому из
commonMainнельзя дотянуться до Android API; - JVM-бэкенд: для Android общий код вместе с
androidMainпревращается в обычный JVM-байткод, а дальше AGP и D8/R8 делают из него DEX, как с любым Kotlin-модулем; - Kotlin/Native: для iOS бэкенд через LLVM собирает нативный код и отдаёт его фреймворком (
.frameworkили XCFramework) с Objective-C заголовками; - JS и Wasm: отдельные бэкенды для веба;
- klib: формат мультиплатформенных библиотек. Это не байткод, а сериализованный IR с метаданными, из которого каждый таргет достраивает свой результат.
expect/actual проверяются при компиляции: каждому expect в общем коде нужна actual-реализация в каждом таргете, иначе сборка упадёт. Зависимости подключаются в блоке sourceSets Gradle, и библиотека в commonMain обязана поддерживать все объявленные таргеты.
Где спотыкаются
Говорят, что общий код сначала компилируется в какой-то промежуточный артефакт, который потом «адаптируется» под платформу. На деле каждый таргет компилирует общие исходники своим бэкендом.
Утверждают, что iOS-часть собирает Xcode. Xcode только вызывает Gradle-задачу (например, embedAndSignAppleFrameworkForXcode) и линкует готовый фреймворк, компилирует его Kotlin/Native. Итоговый фреймворк для Apple-таргетов собирается только на macOS.
Забывают про цену. Сборка Kotlin/Native заметно дольше JVM, а в Swift видно Objective-C API: sealed-иерархии теряют исчерпывающий switch, а suspend-функции и Flow неудобны без обёрток.