Какие ошибки делают команды, когда строят приложение на 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.immediatevsDispatchers.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