Irbisa · Шпаргалка

Kotlin Multiplatform (KMP)

expect/actual, source set-ы, шаринг логики, ktor, SQLDelight, interop с iOS

Senior-разработчик · 16 вопросов

  1. 01

    Что такое Kotlin Multiplatform и что вы делите между Android и iOS?

    KMP компилирует один и тот же Kotlin-код под несколько таргетов (в JVM-байткод для Android и в нативный бинарник через LLVM для iOS), поэтому бизнес-логика пишется один раз, а UI на каждой платформе остаётся своим.

  2. 02

    Что такое объявления expect и actual в KMP и когда интерфейс оказывается лучшим выбором?

    expect объявляет API в commonMain без тела, а каждый платформенный source set даёт свою реализацию actual; если хотя бы у одного таргета её нет, сборка падает.

    // commonMainexpect fun platformName(): String class Greeting {  fun greet(): String = "Hello from ${platformName()}"}…
  3. 03

    Как устроены source sets в KMP (commonMain, iosMain, appleMain) и как с ними связаны Gradle-таргеты?

    У каждого Gradle-таргета свой листовой source set, а default hierarchy template надстраивает над ними общие наборы (iosMain, appleMain, nativeMain) вплоть до commonMain, который компилируется под все таргеты.

    // shared/build.gradle.ktskotlin {  androidTarget()   // AGP 8; AGP 9 replaces it with kotlin { androidLibrary { } }  iosArm64()  iosSimulatorArm64()  jvm()…
  4. 04

    Android и iosArm64 собираются, а iosSimulatorArm64 падает из-за отсутствующего actual: что не так в этой настройке?

    // shared/build.gradle.ktskotlin {  androidTarget()  iosArm64()  iosSimulatorArm64()} // src/commonMain/kotlin/Platform.ktexpect fun deviceModel(): String // src/androidMain/kotlin/Platform.android.ktactual fun deviceModel(): String = android.os.Build.MODEL // src/iosArm64Main/kotlin/Platform.ios.ktactual fun deviceModel(): String = UIDevice.currentDevice.model

    actual лежит в листовом source set iosArm64Main, поэтому он есть только у таргета для устройства; таргет симулятора тоже компилирует commonMain, находит expect и не находит для него actual.

  5. 05

    Из каких библиотек обычно состоит общий KMP-модуль и что должна предоставлять библиотека, чтобы её можно было использовать из commonMain?

    Типичный стек включает Ktor Client с kotlinx.serialization, SQLDelight или Room для хранения, Koin для DI, плюс корутины и kotlinx-datetime; библиотеку можно использовать из commonMain, только если она публикует артефакты для всех ваших таргетов.

    // shared/build.gradle.ktscommonMain.dependencies {  implementation(libs.ktor.client.core)  implementation(libs.ktor.client.content.negotiation)  implementation(libs.ktor.serialization.kotlinx.json)  implementation(libs.koin.core)…
  6. 06

    Как тестировать общий KMP-модуль и почему в commonTest нельзя использовать MockK?

    Тесты в commonTest компилируются и запускаются на каждом таргете, то есть одни и те же проверки выполняются на JVM, на iOS-симуляторе и на десктопе, и именно поэтому JVM-инструменты вроде MockK и Mockito здесь недоступны.

    // commonTestclass CartViewModelTest {   @Test  fun appliesDiscountOnce() = runTest {    val repo = FakeCartRepo(items = listOf(Item("sku-1", price = 100)))…
  7. 07

    KMP, Flutter или React Native: когда что выбирать?

    KMP подходит, когда нужен нативный UI и общая логика, особенно если Android-приложение уже на Kotlin; Flutter или React Native подходят, когда одна команда должна как можно быстрее выпустить один UI на обе платформы.

  8. 08

    Общему коду нужен iOS API, например Keychain: когда вызывать его из Kotlin, когда нужен cinterop, а когда Swift должен реализовать интерфейс?

    Системные фреймворки Apple вызываются из iosMain напрямую через биндинги, которые поставляет Kotlin/Native; сторонней C- или Objective-C-библиотеке нужен .def-файл cinterop; API только на Swift или код, которым владеет iOS-команда, прячется за Kotlin-интерфейсом с реализацией на Swift.

    // commonMain: the contract the shared code depends oninterface SecureStore {  fun read(key: String): String?  fun write(key: String, value: String)}…
  9. 09

    Найдите баг: на Android ответ 404 показывает сообщение об ошибке, а на iOS тот же ответ роняет приложение, несмотря на do/catch.

    // commonMain: Swift calls it as `try await repo.loadProfile(id: id)` inside do/catchclass ApiException(val code: Int) : Exception("HTTP $code") class ProfileRepository(private val client: HttpClient) {  suspend fun loadProfile(id: String): Profile {    val response = client.get("users/$id")    if (!response.status.isSuccess()) {      throw ApiException(response.status.value)    }    return response.body()  }}

    У loadProfile нет @Throws, поэтому Kotlin/Native пропускает в Swift как ошибку только CancellationException.

  10. 10

    Как Swift использует KMP-модуль и где болит мост через Objective-C?

    Kotlin/Native компилирует общий код в Apple-фреймворк с Objective-C-заголовком, и Swift импортирует его как любой другой фреймворк; боль возникает во всём, что Kotlin умеет выразить, а Objective-C не умеет.

  11. 11

    Насколько высоко по стеку может подняться общий код в KMP: можно ли делить ViewModel и навигацию или они остаются нативными?

    ViewModel уже сегодня можно делить через мультиплатформенные артефакты androidx.lifecycle, а навигацию стоит выносить в общий код, только если сам UI на Compose Multiplatform; при SwiftUI на iOS навигация остаётся нативной.

    // commonMain: one ViewModel, bound by both platformsdata class CounterUi(val count: Int = 0, val isLoading: Boolean = false) class CounterViewModel(private val repo: CounterRepo) : ViewModel() {  private val _state = MutableStateFlow(CounterUi())  val state: StateFlow<CounterUi> = _state.asStateFlow()…
  12. 12

    Найдите баг в этом хелпере, который отдаёт общий Flow в Swift, и исправьте его.

    // commonMain: lets Swift observe a Flow through a plain callbackclass FlowWatcher<T : Any>(private val flow: Flow<T>) {  fun watch(onEach: (T) -> Unit) {    val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)    scope.launch {      flow.collect { onEach(it) }    }  }} // Swift: CartViewModel, called from the screen's onAppear// func onAppear() { cartWatcher.watch { items in self.items = items } }

    watch запускает сбор в скоупе, который никто не может отменить.

  13. 13

    Почему многомодульные KMP-проекты отдают iOS один umbrella-фреймворк и что контролирует export()?

    Каждый Kotlin/Native-фреймворк встраивает свой рантайм и свою копию каждой подключённой библиотеки, поэтому два фреймворка дают продублированные несовместимые типы; один umbrella-модуль, зависящий от всех фича-модулей, даёт iOS единый рантайм и цельный API.

    // shared/build.gradle.kts: the umbrella module the iOS app importskotlin {  listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->    target.binaries.framework {      baseName = "Shared"      export(project(":feature:profile"))   // Swift sees these types in full…
  14. 14

    Что KMP добавляет к времени сборки и размеру iOS-приложения и как держать и то и другое под контролем?

    Kotlin/Native компилирует заранее и линкует общий модуль, его рантайм и все библиотеки в один фреймворк, поэтому в сборке iOS появляется неинкрементальный шаг линковки, а приложение прибавляет несколько мегабайт.

    // shared/build.gradle.ktskotlin {  listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->    target.binaries.framework {      baseName = "Shared"      isStatic = true…
  15. 15

    Команда выпускает один продукт на Android и iOS. Как решить, стоит ли внедрять KMP?

    KMP окупается, когда большой и долгоживущий слой бизнес-логики иначе пришлось бы писать дважды, и стоит дороже, чем экономит, когда общая часть получится тонкой или iOS-команда не готова в этом участвовать.

  16. 16

    Как внедрить KMP в два существующих нативных приложения без большого переписывания?

    Начните с одного самодостаточного куска с плотной логикой (API-клиент с моделями или движок расчёта цен), вынесите его в общий модуль, выпустите внутри обоих приложений и только потом шаг за шагом наращивайте общий слой.