Kotlin CoroutinesMiddleCoding

Что такое withContext() и когда его следует использовать?

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

withContext() в Kotlin Coroutines

withContext(context) — suspend-функция, которая выполняет блок кода в указанном CoroutineContext, приостанавливает текущую корутину до завершения блока и возвращает его результат. В отличие от launch и async, она не создаёт новую корутину, а изменяет контекст существующей.

Базовый синтаксис

import kotlinx.coroutines.*

suspend fun readFile(path: String): String =
    withContext(Dispatchers.IO) {
        // Выполняется на IO-пуле, не блокирует main/default
        java.io.File(path).readText()
    }

suspend fun processData(data: String): Result =
    withContext(Dispatchers.Default) {
        // CPU-интенсивная работа на Default-пуле
        heavyComputation(data)
    }

Переключение диспетчеров

Типичный паттерн Android: ViewModel запускает корутину на Main, переключается на IO для данных, возвращается на Main для UI-обновления:

class UserViewModel(private val repo: UserRepository) : ViewModel() {

    fun loadUser(id: String) {
        viewModelScope.launch { // Main dispatcher
            _uiState.value = UiState.Loading

            val user = withContext(Dispatchers.IO) { // переключаемся
                repo.fetchUser(id)  // network/DB call
            } // автоматически возвращаемся на Main

            _uiState.value = UiState.Success(user) // снова на Main
        }
    }
}

withContext vs async/await

// withContext — последовательное выполнение, возвращает значение
val user = withContext(Dispatchers.IO) { repo.getUser(id) }
val orders = withContext(Dispatchers.IO) { repo.getOrders(id) }
// ^ Это последовательно! user загружается, потом orders

// async — параллельное выполнение
val userDeferred = async(Dispatchers.IO) { repo.getUser(id) }
val ordersDeferred = async(Dispatchers.IO) { repo.getOrders(id) }
val user = userDeferred.await()
val orders = ordersDeferred.await()
// ^ Параллельно! Быстрее, если задачи независимы

withContext(NonCancellable)

Специальный случай: cleanup-код, который должен выполниться даже при отмене корутины:

suspend fun saveAndClose(connection: Connection) {
    try {
        connection.save()
    } finally {
        withContext(NonCancellable) {
            connection.close() // выполнится даже если корутина отменена
        }
    }
}

Диспетчеры и их назначение

  • Dispatchers.Main — UI-поток Android, обновление View/State
  • Dispatchers.IO — сеть, файлы, база данных (блокирующий IO)
  • Dispatchers.Default — CPU-интенсивные вычисления, парсинг
  • Dispatchers.Unconfined — не переключает поток; только для специальных случаев

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

  • withContext последовательно: два withContext подряд — это два последовательных переключения, не параллельное выполнение.
  • Частые переключения контекста (withContext в цикле) создают overhead; батчируйте работу.
  • withContext(Dispatchers.IO) не защищает от бесконечного блока — всегда добавляйте withTimeout.
  • Изменение UI-состояния внутри withContext(Dispatchers.IO) вызовет crash на Android (только Main).
  • NonCancellable нельзя использовать за пределами finally — это архитектурная ошибка.
  • withContext не создаёт новую корутину, поэтому CoroutineExceptionHandler родителя перехватит его исключения.
  • Вложенные withContext(Dispatchers.IO) внутри друг друга — лишний overhead без пользы.

Common mistakes

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

What the interviewer is testing

  • Формулирует суть темы «withContext()» своими словами и связывает ее с кодом.
  • Называет механизм: Он сохраняет structured concurrency: вызывающая coroutine suspend-ится, а дочерняя работа отменяется вместе с родителем.
  • Видит production-последствие: Нельзя использовать withContext как универсальный способ спрятать blocking работу без выбора правильного dispatcher-а.

Sources

Related topics