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.

Sources

Related topics