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

Корутины на собеседовании 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 }
Решай алгоритмические задачи как профи

Какие скоупы бывают в Android
| Скоуп | Живёт | Когда использовать |
|---|---|---|
viewModelScope | пока жива ViewModel | основной выбор |
lifecycleScope | пока жив Activity/Fragment | работа с UI |
GlobalScope | всё приложение | почти никогда |
свой CoroutineScope | сколько зададите | в repository, в сервисах |
Почему GlobalScope считается плохой практикой
Корутина в GlobalScope не привязана ни к какому жизненному циклу. Она не отменится при закрытии экрана, продолжит держать ссылки и может обратиться к уничтоженному компоненту.
Это прямая утечка памяти и источник крашей. Классический вопрос-ловушка на собеседовании.
Блок 3. Диспетчеры
Чем отличаются Main, IO и Default
| Диспетчер | Потоков | Для чего |
|---|---|---|
Main | 1 (главный) | обновление 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
В чём разница
launch | async | |
|---|---|---|
| Возвращает | Job | Deferred<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
StateFlow | SharedFlow | |
|---|---|---|
| Начальное значение | обязательно | нет |
| Хранит последнее | да | настраивается |
| Пропускает дубликаты | да | нет |
| Для чего | состояние экрана | события: показать снекбар, навигация |
Практическое правило: состояние — StateFlow, разовые события — SharedFlow. Если использовать StateFlow для событий, после поворота экрана событие повторится.
Сбор с учётом жизненного цикла
lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { render(it) } } }
repeatOnLifecycle останавливает сбор, когда экран уходит в фон, и возобновляет при возврате. Без него подписка продолжает работать в фоне и тратит ресурсы. Это тоже частый вопрос.
Что спрашивают чаще всего
По убыванию частоты:
- Чем корутина отличается от потока.
- Почему
GlobalScope— плохая практика. - Разница
launchиasync. - Когда
IO, а когдаDefault. - Что такое структурированная конкурентность.
- Разница
StateFlowиSharedFlow. - Почему нельзя глотать
CancellationException.
Что запомнить
- Корутина не поток: приостановка освобождает поток, а не блокирует его.
suspendразрешает приостановку, но не переключает поток — за это отвечает диспетчер.- Структурированная конкурентность привязывает корутину к скоупу и исключает утечки.
IOдля ожидания,Defaultдля вычислений,Mainдля UI.try/catchработает только внутри корутины;CancellationExceptionпробрасывайте дальше.- Состояние храните в
StateFlow, разовые события — вSharedFlow.
