Какова роль 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.