Анатомия KMP-проекта: source sets

Где физически лежит общий код, где платформенный, и как компилятор решает, что куда подключать.

Source set (исходный набор) — каталог исходников, привязанный к одной или нескольким целям компиляции; commonMain виден всем целям, платформенные — только своим.

Три ключевых каталога

Структура общего модуля строится вокруг исходных наборов. Минимальный набор для мобильного KMP:

shared/
  src/
    commonMain/kotlin/   // общий код для всех целей
    androidMain/kotlin/  // только Android (JVM)
    iosMain/kotlin/      // только iOS (Native)
    commonTest/kotlin/   // общие тесты
    androidUnitTest/...  // тесты под Android
    iosTest/kotlin/      // тесты под iOS

commonMain — сердце проекта. Сюда идёт всё, что не зависит от платформы. androidMain и iosMain содержат платформенные реализации того, что в общем коде только объявлено.

Кто что видит

Правило видимости простое и одностороннее: платформенные наборы видят общий, но не наоборот. Код в androidMain может вызывать классы из commonMain и использовать Android SDK. Код в commonMain не может обращаться ни к androidMain, ни к Android SDK напрямую — иначе он перестанет собираться под iOS.

commonMain  ←  androidMain  (видит общий + Android SDK)
     ↑
     └────────  iosMain      (видит общий + iOS-интероп)

Иерархия и промежуточные наборы

Между commonMain и листовыми наборами бывают промежуточные. Например, если целей iOS несколько (arm64 для устройства, x64/arm64 для симулятора), их объединяет общий iosMain. Современный шаблон KMP настраивает такую иерархию автоматически через default hierarchy template, и вам редко приходится описывать её руками.

Как работает под капотом

Каждая цель компиляции собирает объединение своих исходных наборов: цель iosArm64 берёт commonMain + (промежуточный) iosMain + свой листовой набор. Цель Android берёт commonMain + androidMain. Компилятор для каждой цели видит ровно тот срез кода, что ей положен, и проверяет типы именно в этом контексте. Поэтому expect-объявление в общем наборе обязано иметь actual в каждом платформенном — иначе цель соберёт неполный API.

Частые ошибки

Новички складывают всё подряд в commonMain, включая платформенные импорты, и удивляются, что Android собирается, а iOS — нет. Помните: commonMain — самый «бедный» по доступному API набор. Если что-то нужно из платформы, это либо expect/actual, либо код прямо в платформенном наборе. Ещё ошибка — путать androidMain (основной Android-код) с androidUnitTest (тесты); зависимости и видимость у них разные.

Итоги

  • commonMain — общий код; androidMain/iosMain — платформенный.
  • Платформенные наборы видят общий, общий их — нет.
  • Цель компилируется как объединение своих source set'ов.
  • Платформенный API в commonMain недопустим — только через expect/actual.
Проверьте себя
1. Может ли код в commonMain напрямую обращаться к Android SDK?
AДа, всегда
BНет, иначе он не соберётся под iOS
CТолько в тестах
DТолько если включить флаг
2. Что собирает цель iosArm64?
AТолько iosMain
BТолько commonMain
CОбъединение commonMain, промежуточного iosMain и своего листового набора
DВесь проект включая androidMain