SprintCode.pro

Подготовка к алгоритмическим задачам

Super

Корутины на собеседовании Android: вопросы и ответы

13 мин чтения
собеседование
kotlin
android

Коротко

БлокЧто спрашивают
Основычто такое suspend, чем отличается от потока
СкоупыviewModelScope, lifecycleScope, структурированная конкурентность
ДиспетчерыMain, IO, Default — когда какой
Билдерыlaunch против async
Ошибкиtry/catch, CoroutineExceptionHandler, SupervisorJob
Flowхолодные и горячие потоки, StateFlow против SharedFlow

Блок 1. Основы

Что такое корутина и чем отличается от потока

Корутина — это не поток. Это приостанавливаемая задача, которая выполняется на потоке, но может освободить его на время ожидания.

ПотокКорутина
Стоимость создания~1 МБ стеканесколько байт
Переключениечерез ядро ОСв пространстве приложения
Сколько можно запуститьтысячимиллионы
Блокировказанимает потокосвобождает поток

Ключевая мысль: приостановка корутины не блокирует поток. Пока корутина ждёт ответа от сети, поток обслуживает другие корутины.

Что делает ключевое слово suspend

Помечает функцию как приостанавливаемую: её можно вызвать только из корутины или другой suspend-функции.

suspend fun loadUser(id: String): User { return api.getUser(id) // приостановка без блокировки потока }

Под капотом компилятор превращает такую функцию в конечный автомат (CPS-преобразование) и добавляет скрытый параметр Continuation. Про это любят спрашивать на позициях выше junior.

Важно: suspend сам по себе не переключает поток. Он лишь позволяет приостановку. За поток отвечает диспетчер.

Блок 2. Скоупы и структурированная конкурентность

Что такое структурированная конкурентность

Принцип: у каждой корутины есть родитель, и родитель не завершится, пока не завершатся дети. Отмена родителя отменяет всех потомков.

Это решает главную проблему асинхронности — утечки задач. Корутина не может «потеряться»: она всегда привязана к скоупу.

viewModelScope.launch { val user = loadUser() // при очистке ViewModel отменится автоматически _state.value = user }
Пройди собеседование в топ-компанию
Платформа для подготовки

Решай алгоритмические задачи как профи

✓ Популярные алгоритмы✓ Разбор решений✓ AI помощь
Начать сейчас
Программист за работой

Какие скоупы бывают в Android

СкоупЖивётКогда использовать
viewModelScopeпока жива ViewModelосновной выбор
lifecycleScopeпока жив Activity/Fragmentработа с UI
GlobalScopeвсё приложениепочти никогда
свой CoroutineScopeсколько зададитев repository, в сервисах

Почему GlobalScope считается плохой практикой

Корутина в GlobalScope не привязана ни к какому жизненному циклу. Она не отменится при закрытии экрана, продолжит держать ссылки и может обратиться к уничтоженному компоненту.

Это прямая утечка памяти и источник крашей. Классический вопрос-ловушка на собеседовании.

Блок 3. Диспетчеры

Чем отличаются Main, IO и Default

ДиспетчерПотоковДля чего
Main1 (главный)обновление UI
IOдо 64сеть, диск, база данных
Defaultпо числу ядервычисления, парсинг, сортировка
Unconfinedлюбойпочти никогда, только для тестов
viewModelScope.launch { // Main по умолчанию val data = withContext(Dispatchers.IO) { repository.fetchFromNetwork() // сеть на IO } val parsed = withContext(Dispatchers.Default) { heavyParsing(data) // вычисления на Default } textView.text = parsed // снова Main }

Частая ошибка: использовать IO для тяжёлых вычислений. У него много потоков, рассчитанных на ожидание, и загрузка их вычислениями душит процессор.

Хорошая практика: переключение диспетчера должно быть внутри repository, а не в ViewModel. Тогда вызывающий код не думает про потоки.

Блок 4. launch и async

В чём разница

launchasync
ВозвращаетJobDeferred<T>
Результатнетесть, через await()
Исключениесразу вверх по иерархиипри вызове await()
Когда«запусти и забудь»нужен результат
// параллельные запросы viewModelScope.launch { val user = async { loadUser() } val posts = async { loadPosts() } // оба запроса уже выполняются параллельно render(user.await(), posts.await()) }

Ловушка: если написать async { }.await() подряд, параллельности не будет — это последовательное выполнение с лишними накладными расходами.

// НЕПРАВИЛЬНО — последовательно val user = async { loadUser() }.await() val posts = async { loadPosts() }.await()

Блок 5. Обработка ошибок

Почему try/catch иногда не ловит

Исключение в launch распространяется вверх по иерархии корутин, а не возвращается в место вызова.

// не поймает try { scope.launch { throw RuntimeException() } } catch (e: Exception) { } // поймает scope.launch { try { риск() } catch (e: Exception) { } }

Правило: try/catch должен быть внутри корутины.

Job против SupervisorJob

Обычный Job: падение одного ребёнка отменяет родителя и всех остальных детей.

SupervisorJob: дети падают независимо, остальные продолжают работу.

val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main) scope.launch { throw Exception() } // упадёт только эта scope.launch { продолжитРаботу() } // выживет

viewModelScope внутри использует именно SupervisorJob — поэтому ошибка в одной операции не роняет всю ViewModel.

CoroutineExceptionHandler

Последний рубеж для необработанных исключений в launch. С async не работает — там исключение приходит при await().

val handler = CoroutineExceptionHandler { _, e -> Log.e("App", "необработанная ошибка", e) } scope.launch(handler) { risky() }

Отмена — это тоже исключение

CancellationException используется для отмены и не должна перехватываться.

try { delay(1000) } catch (e: Exception) { // плохо: проглотит отмену log(e) } try { delay(1000) } catch (e: CancellationException) { throw e // пробрасываем дальше } catch (e: Exception) { log(e) }

Это один из самых частых вопросов-ловушек для позиций middle и выше.

Блок 6. Flow

Чем Flow отличается от suspend-функции

Suspend-функция возвращает одно значение. Flowпоследовательность значений во времени.

fun observeUsers(): Flow<List<User>> = flow { while (true) { emit(api.getUsers()) delay(5000) } }

Холодные и горячие потоки

Холодный (flow { }): код выполняется заново для каждого подписчика. Нет подписчиков — ничего не происходит.

Горячий (StateFlow, SharedFlow): существует независимо от подписчиков, все получают одни и те же значения.

StateFlow против SharedFlow

StateFlowSharedFlow
Начальное значениеобязательнонет
Хранит последнееданастраивается
Пропускает дубликатыданет
Для чегосостояние экранасобытия: показать снекбар, навигация

Практическое правило: состояние — StateFlow, разовые события — SharedFlow. Если использовать StateFlow для событий, после поворота экрана событие повторится.

Сбор с учётом жизненного цикла

lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { render(it) } } }

repeatOnLifecycle останавливает сбор, когда экран уходит в фон, и возобновляет при возврате. Без него подписка продолжает работать в фоне и тратит ресурсы. Это тоже частый вопрос.

Что спрашивают чаще всего

По убыванию частоты:

  1. Чем корутина отличается от потока.
  2. Почему GlobalScope — плохая практика.
  3. Разница launch и async.
  4. Когда IO, а когда Default.
  5. Что такое структурированная конкурентность.
  6. Разница StateFlow и SharedFlow.
  7. Почему нельзя глотать CancellationException.

Что запомнить

  • Корутина не поток: приостановка освобождает поток, а не блокирует его.
  • suspend разрешает приостановку, но не переключает поток — за это отвечает диспетчер.
  • Структурированная конкурентность привязывает корутину к скоупу и исключает утечки.
  • IO для ожидания, Default для вычислений, Main для UI.
  • try/catch работает только внутри корутины; CancellationException пробрасывайте дальше.
  • Состояние храните в StateFlow, разовые события — в SharedFlow.