Kotlin MultiplatformMiddleTechnical

Какова роль kotlinx.coroutines в KMP и как он настраивается для каждой платформы?

kotlinx.coroutines предоставляет единый async API (suspend, Flow, structured concurrency) для всех KMP-платформ. Настройка отличается по dispatcher-ам: на iOS нет автоматического Main dispatcher, на Android он привязан к Looper.

Роль kotlinx.coroutines в KMP

kotlinx.coroutines — единственная официальная библиотека structured concurrency, поддерживающая все KMP targets. В commonMain она предоставляет suspend-функции, Flow, CoroutineScope и стандартные dispatcher-ы. На каждой платформе реализация dispatcher-ов отличается.

Платформенные различия dispatcher-ов

  • Android: Dispatchers.Main реализован через Android Looper; доступен автоматически при наличии kotlinx-coroutines-android в зависимостях.
  • iOS (Kotlin/Native): Dispatchers.Main требует явной инициализации; используется kotlinx-coroutines-core без дополнительного артефакта, но основной поток привязан к RunLoop.
  • JVM/Desktop: Dispatchers.Main зависит от UI-фреймворка (Swing, JavaFX); без артефакта kotlinx-coroutines-swing вызов бросает исключение.

Конфигурация в KMP-проекте

// build.gradle.kts
kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.coroutines.core)
        }
        androidMain.dependencies {
            implementation(libs.kotlinx.coroutines.android) // добавляет Dispatchers.Main для Android
        }
    }
}

// commonMain: dispatcher инжектируется, не хардкодится
class UserRepository(
    private val api: UserApi,
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.Default
) {
    suspend fun fetchUser(id: String): User = withContext(ioDispatcher) {
        api.getUser(id)
    }
}

// commonMain: Flow работает на всех платформах одинаково
class AuthViewModel(private val repo: TokenRepository) {
    val authState: Flow<AuthState> = repo.observeToken()
        .map { token -> if (token != null) AuthState.LoggedIn else AuthState.LoggedOut }
        .catch { emit(AuthState.Error(it)) }
}

// iosMain: мост к Swift через callback (до появления async/await bridge)
fun observeAuthState(
    viewModel: AuthViewModel,
    scope: CoroutineScope,
    onState: (AuthState) -> Unit
) {
    scope.launch {
        viewModel.authState.collect { onState(it) }
    }
}

Structured concurrency на платформенной границе

На iOS корутины запускаются в scope, который привязан к lifecycle View или ViewController. При переходе на другой экран scope нужно отменять явно — автоматического механизма, как viewModelScope в Android, нет. Типовое решение — создавать scope в init и отменять в onDestroy / deinit.

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

  • Использовать Dispatchers.Main напрямую в commonMain — на iOS без корректной инициализации RunLoop бросает IllegalStateException.
  • Предполагать наличие Dispatchers.IO на всех платформах — на Kotlin/Native его нет; используйте Dispatchers.Default или собственный dispatcher.
  • Запускать корутину без scope на iOS — утечка памяти и продолжение работы после уничтожения экрана.
  • Не добавлять kotlinx-coroutines-android в androidMain — Dispatchers.Main не резолвится, приложение крашится при первом обращении к UI.
  • Использовать GlobalScope в shared-коде — нет отмены при lifecycle-событиях, сложно тестировать.
  • Передавать CoroutineScope через границу iOS без обёртки — Swift видит его как непрозрачный тип, управлять lifecycle нельзя.
  • Игнорировать CoroutineExceptionHandler на iOS — необработанные исключения в корутинах могут крашить приложение без stack trace.

Common mistakes

  • Объяснять «роль kotlinx.coroutines в KMP» только как синтаксис и не описывать поведение runtime/compiler.
  • Игнорировать важный риск: Нельзя писать common API, который подразумевает Android Main dispatcher или JVM executor на всех targets.
  • Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.

What the interviewer is testing

  • Формулирует суть темы «роль kotlinx.coroutines в KMP» своими словами и связывает ее с кодом.
  • Называет механизм: Platform-specific конфигурация касается dispatcher-ов, lifecycle scope, test dispatcher и мостов к Swift/Android UI.
  • Видит production-последствие: Нельзя писать common API, который подразумевает Android Main dispatcher или JVM executor на всех targets.

Sources

Related topics