Kotlin MultiplatformSeniorSystem design

Каковы основные сложности при внедрении KMP в существующий проект?

Начинайте с изолированного shared-модуля под один сценарий — например, слой данных. Главный риск: общий модуль без явного владельца замедляет обе команды и накапливает платформенные утечки.

Основные сложности при внедрении KMP

Внедрение KMP в живой проект отличается от старта с нуля: нужно сохранить работоспособность Android и iOS, не сломать CI и договориться с двумя командами об ownership общего кода. Типичные сложности делятся на технические, организационные и операционные.

Технические сложности

  • Gradle-конфигурация — добавление KMP-плагина меняет структуру source set-ов; существующие Android-модули нужно мигрировать на androidTarget() вместо com.android.library.
  • Platform-specific зависимости — библиотеки без KMP-артефактов (OkHttp, некоторые Room API) требуют expect/actual-обёрток или замены на multiplatform-аналоги (Ktor, SQLDelight).
  • Swift/ObjC interop — публичный API из commonMain транслируется через Kotlin/Native; suspend-функции, generics и sealed-классы требуют дополнительного слоя адаптеров для Swift.
  • Параллельные сборки — XCFramework нужно генерировать в CI, что увеличивает время сборки и требует macOS-агентов.

Стратегия внедрения

Начинайте с одного изолированного сценария: например, слой сетевых запросов или локального кэша. Убедитесь, что shared-модуль не тянет за собой Android SDK.

// build.gradle.kts (shared-модуль)
kotlin {
    androidTarget()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        commonMain.dependencies {
            implementation(libs.ktor.client.core)
            implementation(libs.kotlinx.coroutines.core)
        }
        androidMain.dependencies {
            implementation(libs.ktor.client.okhttp)
        }
        iosMain.dependencies {
            implementation(libs.ktor.client.darwin)
        }
    }
}

// commonMain: контракт без платформенных деталей
class TokenRepository(private val api: AuthApi, private val store: TokenStore) {
    suspend fun refreshIfNeeded(): String {
        val cached = store.read() ?: return fetchAndStore()
        return if (cached.isExpired()) fetchAndStore() else cached.value
    }
    private suspend fun fetchAndStore(): String {
        val token = api.refresh()
        store.write(token)
        return token.value
    }
}

Организационные сложности

  • Ownership — без явного владельца shared-модуль деградирует: Android-команда добавляет Android-зависимости, iOS-команда не может их заменить.
  • Контракт с iOS — нужно договориться об API-стабильности до публикации XCFramework; любой breaking change требует синхронного релиза на обеих платформах.
  • Обучение — iOS-разработчики должны понимать Kotlin-идиомы (Flow, sealed class), Android-разработчики — ограничения Kotlin/Native.

Операционные сложности

  • CI должен собирать и тестировать все targets; без этого проблемы обнаруживаются только на устройстве.
  • Rollback-план: если shared-код ломает iOS, нужна возможность быстро вернуться к нативной реализации без релиза Android.
  • Observability: логи и краш-репорты должны коррелировать с версией XCFramework, а не только с версией приложения.

Подводные камни

  • Добавление Android-зависимости в commonMain вместо androidMain — компилируется, но ломает iOS-сборку в runtime.
  • Использование Dispatchers.Main в common-коде без expect/actual — на iOS Main dispatcher не инициализирован автоматически до вызова MainScope().
  • Публикация suspend-функций напрямую в Swift без обёртки — iOS-код получает нечитаемые callback-и через KotlinUnit.
  • Отсутствие hermetic-тестов для common-модуля — ошибки в бизнес-логике обнаруживаются только при запуске на устройстве.
  • Один XCFramework на весь проект — любое изменение требует пересборки и обновления SPM/CocoaPods-зависимости в iOS-проекте.
  • Игнорирование iosSimulatorArm64 при добавлении нового target — разработчики на Apple Silicon не могут запустить симулятор.
  • Shared ViewModel с Android lifecycle — ViewModel из Jetpack недоступен в commonMain; нужен KMP-совместимый аналог (decompose, moko-mvvm).
  • Миграция всего проекта сразу — резко увеличивает зону риска; iterative migration по модулям безопаснее.

Common mistakes

  • Объяснять «внедрение KMP в существующий проект» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Главный риск - создать общий модуль, который замедляет обе команды и не имеет ответственного владельца.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «внедрение KMP в существующий проект» своими словами и связывает ее с кодом.
  • Называет механизм: Нужны границы ownership, CI для всех targets, контракт с iOS-командой, observability, rollback и migration plan.
  • Видит production-последствие: Главный риск - создать общий модуль, который замедляет обе команды и не имеет ответственного владельца.

Sources

Related topics