Kotlin MultiplatformMiddleSystem design
Как структурировать KMP-проект (shared module, платформо-зависимые модули)?
KMP-проект состоит из shared-модуля (commonMain/androidMain/iosMain source sets), Android Gradle-модуля и Xcode-проекта для iOS; shared компилируется в AAR для Android и XCFramework для iOS.
Структура KMP-проекта
Стандартный KMP-проект состоит из трёх слоёв: общий shared-модуль с кросс-платформенной логикой, платформо-зависимые модули (androidApp, iosApp) и, при необходимости, дополнительные feature-модули. Генератор на kmp.jetbrains.com создаёт правильный скелет автоматически.
Типовая структура директорий
my-kmp-project/
├── androidApp/ # Android-приложение (модуль Gradle)
│ ├── src/main/
│ └── build.gradle.kts
├── iosApp/ # Xcode-проект
│ ├── iosApp.xcodeproj/
│ └── iosApp/
│ └── ContentView.swift
├── shared/ # Общий KMP-модуль
│ ├── src/
│ │ ├── commonMain/kotlin/ # Общая бизнес-логика
│ │ ├── commonTest/kotlin/ # Общие тесты
│ │ ├── androidMain/kotlin/ # Android-реализации expect
│ │ ├── androidTest/kotlin/
│ │ ├── iosMain/kotlin/ # iOS-реализации expect
│ │ └── iosTest/kotlin/
│ └── build.gradle.kts
├── build.gradle.kts # Корневой build файл
└── settings.gradle.kts
Конфигурация shared/build.gradle.kts
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.kotlinxSerialization)
}
kotlin {
androidTarget {
compilations.all {
kotlinOptions.jvmTarget = "11"
}
}
iosArm64()
iosSimulatorArm64()
iosX64() // для Intel Mac
sourceSets {
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(libs.kotlinx.serialization.json)
implementation(libs.ktor.client.core)
}
commonTest.dependencies {
implementation(libs.kotlin.test)
}
androidMain.dependencies {
implementation(libs.ktor.client.okhttp)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
}
android {
namespace = "com.example.shared"
compileSdk = 34
defaultConfig { minSdk = 24 }
}
Внутренняя организация commonMain
Внутри commonMain рекомендуется layer-архитектура по пакетам:
commonMain/kotlin/com/example/
├── data/
│ ├── remote/ # Ktor-клиенты, DTO
│ ├── local/ # SQLDelight queries
│ └── repository/ # Реализации репозиториев
├── domain/
│ ├── model/ # Domain-модели
│ ├── repository/ # Интерфейсы репозиториев
│ └── usecase/ # Use cases / interactors
├── presentation/ # ViewModels (если используется KMP ViewModel)
└── Platform.kt # expect-декларации
Несколько feature-модулей
Для больших проектов shared делится на feature-модули:
// settings.gradle.kts
include(":androidApp")
include(":shared:core")
include(":shared:feature-auth")
include(":shared:feature-feed")
// shared/feature-auth/build.gradle.kts
kotlin {
androidTarget()
iosArm64(); iosSimulatorArm64()
sourceSets {
commonMain.dependencies {
api(project(":shared:core"))
}
}
}
Подключение shared к androidApp
// androidApp/build.gradle.kts
dependencies {
implementation(project(":shared"))
}
Подключение shared к Xcode
Запускается задача Gradle, которая создаёт XCFramework, затем он подключается в Xcode как Framework Search Path или через SPM:
# Сборка debug-фреймворка для симулятора
./gradlew :shared:assembleSharedDebugXCFramework
Подводные камни
- iosMain не создаётся автоматически при нескольких iOS-таргетах — нужно явно создать промежуточный source set и настроить
dependsOn(commonMain)для iosArm64Main, iosSimulatorArm64Main и iosX64Main. - androidTarget() против android() — в Kotlin 1.9+ используется
androidTarget(); старыйandroid()депрекирован и вызовет предупреждения при сборке. - Неправильный namespace в android-блоке — если namespace не совпадает с applicationId в манифесте, R-классы не будут найдены.
- Зависимости должны поддерживать нужные таргеты — Android-only библиотека, добавленная в commonMain, вызовет ошибку линковки на iOS; всегда проверяйте поддерживаемые таргеты.
- iosApp не является Gradle-модулем — Xcode-проект управляется отдельно; изменения в shared требуют пересборки фреймворка и обновления в Xcode.
- Конфликты версий зависимостей между модулями — при разбивке на feature-модули один модуль может подтянуть несовместимую версию транзитивной зависимости; используйте platform BOM или явно выравнивайте версии.
- Gradle Configuration Cache не поддерживается некоторыми KMP-плагинами — включение
org.gradle.configuration-cache=trueможет ломать сборку; проверяйте совместимость.
Common mistakes
- Объяснять «структура KMP-проекта» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Плохая структура приводит к циклическим зависимостям, platform leakage и невозможности тестировать common logic.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «структура KMP-проекта» своими словами и связывает ее с кодом.
- Называет механизм: Gradle targets, sourceSets и dependencies определяют, какой код и библиотеки доступны конкретной компиляции.
- Видит production-последствие: Плохая структура приводит к циклическим зависимостям, platform leakage и невозможности тестировать common logic.