Kotlin CoroutinesMiddleExperience

Какие ошибки делают команды, когда строят приложение на Kotlin Coroutines как web/backend-проект без учёта mobile constraints?

Типичные ошибки: игнорирование lifecycle (аналог серверных вечных потоков), отсутствие backpressure, блокировка Main-потока, ignoring cancellation, чрезмерное использование GlobalScope и отсутствие bounded parallelism.

Ошибки команд, переносящих web/backend-мышление на мобильные корутины

Backend-разработчики привыкают к другой модели: long-lived процессы без UI, неограниченные потоки, всегда активное соединение. На мобильном эти привычки создают проблемы.

Ошибка 1: игнорирование lifecycle

На сервере запрос живёт от начала до конца. На мобильном экран может уйти в фон, повернуться или уничтожиться. Корутины без привязки к lifecycle продолжают работать.

// НЕПРАВИЛЬНО — GlobalScope как на сервере
class BadActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        GlobalScope.launch { // не отменится при destroy!
            val data = api.fetchData()
            runOnUiThread { updateUI(data) } // Activity может быть уничтожена
        }
    }
}

// ПРАВИЛЬНО
class GoodViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch { // отменится в onCleared()
            _state.value = repo.fetchData()
        }
    }
}

Ошибка 2: блокирующие вызовы без withContext

На сервере thread pool большой — blocking IO не критично. На Android Main thread один, и его блокировка — ANR.

// НЕПРАВИЛЬНО — блокирующий IO в Main-диспетчере
viewModelScope.launch { // Main по умолчанию
    val file = File("/data/large.json").readText() // БЛОКИРУЕТ UI!
    processData(file)
}

// ПРАВИЛЬНО
viewModelScope.launch {
    val file = withContext(Dispatchers.IO) {
        File("/data/large.json").readText()
    }
    processData(file) // обратно на Main
}

Ошибка 3: игнорирование CancellationException

На сервере обычно ловят все исключения. В корутинах CancellationException — механизм structured cancellation, его нельзя глотать.

// НЕПРАВИЛЬНО — поглощение CancellationException
viewModelScope.launch {
    try {
        doWork()
    } catch (e: Exception) { // ловит ВСЕ, включая CancellationException!
        Log.e("Error", "failed", e)
    }
}

// ПРАВИЛЬНО
viewModelScope.launch {
    try {
        doWork()
    } catch (e: CancellationException) {
        throw e // переброс обязателен!
    } catch (e: Exception) {
        Log.e("Error", "failed", e)
    }
}

Ошибка 4: неограниченный параллелизм

Backend запускает столько потоков, сколько нужно. На мобильном чрезмерный параллелизм убивает батарею и память.

// НЕПРАВИЛЬНО — 1000 параллельных запросов
suspend fun loadAll(ids: List<Long>) = coroutineScope {
    ids.map { id -> async { api.getItem(id) } }.awaitAll()
}

// ПРАВИЛЬНО — ограничить до разумного числа
suspend fun loadAll(ids: List<Long>) = coroutineScope {
    val semaphore = Semaphore(10)
    ids.map { id ->
        async {
            semaphore.withPermit { api.getItem(id) }
        }
    }.awaitAll()
}

Ошибка 5: отсутствие backpressure

На сервере producer и consumer часто равноправны. В мобильном UI не успевает обрабатывать быстрые потоки данных.

// НЕПРАВИЛЬНО — каждое событие триггерит тяжёлую операцию
button.setOnClickListener {
    viewModelScope.launch { api.sendData() } // параллельные запросы при быстрых кликах
}

// ПРАВИЛЬНО — debounce через Flow
button.clicks() // kotlinx-coroutines-android
    .debounce(300)
    .onEach { viewModel.sendData() }
    .launchIn(lifecycleScope)

Ошибка 6: длинные polling-циклы без учёта Doze mode

Серверный демон с while(true) { delay(1000) } работает вечно. На Android Doze mode заморозит приложение через 30 минут в фоне. Для background-работы нужен WorkManager.

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

  • Использование runBlocking в продакшн-коде Android — блокирует поток, вызывает ANR на Main или starvation на IO.
  • Создание CoroutineScope вручную без SupervisorJob — ошибка в одной дочерней корутине отменяет весь скоуп.
  • Отсутствие timeout на сетевых запросах: withTimeout(5000) { api.call() } — без этого корутина ждёт вечно при проблемах с сетью.
  • SharedFlow с replay=0 как замена EventBus без учёта: подписчик пропускает события, пришедшие до подписки.
  • Игнорирование Dispatchers.Main.immediate vs Dispatchers.Main: immediate выполняет синхронно если уже на Main, обычный — всегда через dispatch-очередь.
  • Бесконечный Flow без stateIn переподписывается при каждом collect, создавая дублирующие запросы к API.
  • Вызов flow.first() для получения начального значения StateFlow вместо stateFlow.value — лишний overhead.

What hurts your answer

  • Перечислять ошибки без объяснения причин
  • Не отличать beginner mistakes от production failure modes
  • Не предлагать процесс, который предотвращает повторение ошибок

What they're listening for

  • Знает типичные ошибки при работе с Kotlin Coroutines
  • Понимает причины ошибок
  • Предлагает практики prevention и early detection

Related topics