Ты читал документацию. Смотрел видео. Писал viewModelScope.launch десятки раз. Можешь объяснить, что такое suspend-функция. Но на собеседовании, после третьего уточняющего вопроса про корутины, уверенность испаряется.
Проблема не в том, что ты не знаешь Coroutines. Проблема в том, что ты знаешь их на уровне API, а собеседование проверяет на уровне понимания. Разница — пропасть. И именно в неё падают 8 из 10 кандидатов с 1–2 годами опыта.
Ниже — разбор типичных вопросов, на которых сыплются. Не теория из документации, а то, что реально спрашивают и где именно ломаются ответы.
Ошибка №1: «Dispatchers.IO — для тяжёлых вычислений, Dispatchers.Default — для сети»
Что обычно отвечают
«Dispatchers.IO нужен для ввода-вывода: сеть, файлы, база данных. Dispatchers.Default для CPU-тяжёлых задач: парсинг JSON, сортировка.»
Почему это провал
Это заученная фраза из первого попавшегося туториала. Она формально верна, но интервьюер сразу копнёт глубже:
— А сколько потоков в Dispatchers.IO и в Dispatchers.Default? — Что произойдёт, если я запущу 200 сетевых запросов на Dispatchers.Default? — Можно ли переключиться внутри корутины с одного диспетчера на другой? Как?
Что ожидают услышать
Кандидат понимает модель пулов потоков, а не просто запоминает ярлыки:
Dispatchers.Defaultограничен количеством ядер CPU (Runtime.getRuntime().availableProcessors()). Он для задач, которые грузят процессор.Dispatchers.IOимеет пул до 64 потоков (или больше, в зависимости от версии) и оптимизирован под блокирующие операции. Потоки могут переиспользоваться между IO и Default.- Сетевые запросы через OkHttp/Retrofit уже асинхронны внутри. Запускать их на
Dispatchers.IOчасто избыточно — достаточноDispatchers.Defaultили вообщеUnconfined, если библиотека сама переключает контекст. - Переключение — через
withContext(Dispatchers.IO) { ... }. Это не создаёт новую корутину, а перевешивает текущую на другой диспетчер.
Как звучит сильный ответ
«Dispatchers — это не про тип задачи, а про характер нагрузки. IO — для операций, где поток большую часть времени ждёт. Default — для операций, где поток реально считает. Если я запускаю Retrofit-запрос, мне не нужен IO, потому что OkHttp сам управляет своими потоками. А вот если я читаю большой файл побайтово — тогда да,
withContext(Dispatchers.IO).»
Ошибка №2: «structured concurrency — это когда корутины вложены друг в друга»
Что обычно отвечают
«Structured concurrency означает, что дочерние корутины привязаны к родителю и отменяются вместе с ним.»
Почему это ошибка
Это определение из вики, а не понимание. Интервьюер спросит:
- Как распространяется отмена и исключения между родителем и детьми?
- Что будет, если дочерняя корутина выбросит исключение? Умрёт ли родитель?
- А если я не хочу, чтобы умирал?
Кандидат с заученным определением здесь зависает.
Что ожидают услышать
Понимание иерархии отмены и распространения исключений:
- Корутина, запущенная в
CoroutineScope, привязана к егоJob. Отмена scope → отмена всех детей. Это и есть структура. GlobalScope.launch— нарушение structured concurrency. Корутина живёт сама по себе, не привязана к lifecycle. В Android это почти всегда баг.- Если дочерняя корутина падает с исключением, по умолчанию родитель тоже отменяется, а вместе с ним — все остальные дети. Это fail-fast behaviour.
- Чтобы изолировать падение —
SupervisorJobилиsupervisorScope. Тогда падение одной ветки не влияет на остальные.
Как звучит сильный ответ
«Structured concurrency — это гарантия, что у каждой корутины есть родитель и она не переживёт свой scope. В Android это критично: viewModelScope привязан к ViewModel, lifecycleScope — к Lifecycle. Если ViewModel очищается, все корутины отменяются, и я не получаю утечку. Если нужно, чтобы падение одной задачи не убивало остальные, я использую SupervisorJob — например, при загрузке независимых секций экрана параллельно.»
Ошибка №3: «Утечка памяти через корутины? Ну, я же в viewModelScope запускаю»
Что обычно отвечают
«Если использовать viewModelScope или lifecycleScope, утечек не будет, потому что scope сам отменяется.»
Почему это ошибка
Частично верно, но интервьюер проверит:
— Что если внутри корутины ты держишь ссылку на Activity? — А если используешь GlobalScope или создаёшь свой CoroutineScope и забываешь отменить? — Что будет, если suspend-функция бесконечно ждёт в delay() или в блокирующем вызове?
Что ожидают услышать
Понимание реальных сценариев утечек:
viewModelScopeзащищает от отмены корутины, но не от утечки объектов внутри неё. Если внутриlaunchты захватил ссылку наActivityилиContext, и корутина живёт дольше, чем Activity — утечка.GlobalScope.launch— корутина живёт, пока не завершится сама. Если внутри бесконечный цикл илиdelay(Long.MAX_VALUE)— это утечка.- Собственный
CoroutineScope(Dispatchers.Main)без вызоваcancel()— та же проблема. Нужно отменять вonDestroy/onCleared. - Блокирующие вызовы (
Thread.sleep, синхронный IO) на Main не отменяются черезJob.cancel(). Корутина не может прервать блокирующий вызов — только дождаться его завершения.
Как звучит сильный ответ
«viewModelScope закрывает 90% случаев, но не все. Если я внутри корутины держу ссылку на View или Context, и корутина переживает их lifecycle — утечка. Поэтому я передаю applicationContext, если нужен Context, или использую WeakReference. И я помню, что cancel() корутины не прерывает блокирующий код — только suspend-точки. Если нужно прервать тяжёлую операцию, я проверяю isActive или использую withTimeout.»
Ошибка №4: «SupervisorJob — это чтобы корутины не отменялись»
Что обычно отвечают
«SupervisorJob нужен, чтобы при падении одной корутины не отменялись остальные.»
Почему это ошибка
Ответ в начале верен, но плоско. Интервьюер уточнит:
- В чём разница между
supervisorScopeиCoroutineScope(SupervisorJob())? - Если использую
SupervisorJob, исключения вообще куда-то деваются? - Что будет, если не поставить
CoroutineExceptionHandler?
Что ожидают услышать
SupervisorJobменяет направление распространения исключений: ребёнок больше не отменяет родителю и брата. Но исключение не исчезает — оно поднимается доCoroutineExceptionHandlerили крашит приложение.supervisorScope { }— структурный аналог. Создаёт scope с SupervisorJob внутри текущей кору. Если упадёт самsupervisorScope, а не ребёнок — родитель всё равно отменится.CoroutineScope(SupervisorJob() + Dispatchers.Main)— создаёт внешний scope. Его нужно отменять вручную.- Без
CoroutineExceptionHandlerв scope сSupervisorJobнеобработанное исключение вложиит к крашу. Это частый сценарий провала.
Как звучит сильный ответ
«SupervisorJob изолирует детей, но не проглатывает исключения. Мне всё равно нужен CoroutineExceptionHandler, иначе приложение упадёт. Я использую supervisorScope, когда загружаю несколько независимых блоков UI: если один упадёт, остальные доедут. Логирую ошибку через handler и показываю заменитель вместо краша.»
Ошибка №5: «withContext и async — это одно и то же, просто синтаксис другой»
Что обычно отвечают
«async возвращает Deferred, а withContext — результат напрямую. Но по сути оба переключают контекст.»
Почему это ошибка
Интервьюер спросит:
- Что будет, если вызвать
asyncи не вызватьawait()? withContextвнутриasync— это норма?- Если нужно запустить 10 запросов параллельно и дождаться всех?
Что ожидают услышать
asyncзапускает новую корутину и возвращаетDeferred. Результат не появится, пока не вызовешьawait(). Если не вызвать — корутина отработает впустую, а исключение потеряется.withContextне создаёт новую корутину. Это suspend-функция, которая переключает диспетчер. Она приостанавливает текущую корутину до завершения.- Для параллельных запросов — несколько
async+awaitAll(). ИлиcoroutineScope { }с несколькимиasync— тогда structured concurrency гарантирует отмену всех при падении одного. withContextвнутриasync— нормально, если нужно сменить диспетчер для части работы.
Как звучит сильный ответ
«async — это запуск параллельной задачи. withContext — переключение внутри текущей задачи. Если нужно параллельно загрузить профиль и уведомления — делаю два async и awaitAll. Если переключиться на IO для чтения кэша — withContext. И я помню: async без await — молча проглоченное исключение.»
Ошибка №6: «Отмена корутины — это просто вызвать job.cancel()»
Что обычно отвечают
«Вызываю job.cancel(), и корутина останавливается.»
Почему это ошибка
- Если внутри корутины цикл
for (i in 1..1_000_000)без suspend-точек? - Что такое cooperative cancellation?
cancel()vscancelAndJoin()?
Что ожидают услышать
- Отмена в Coroutines — кооперативная. Корутина не убивается принудительно. Она проверяет флаг отмены только в suspend-точках:
delay(),yield(),withContext(),await(). - Если плотный CPU-цикл без suspend-вызовов,
cancel()не прервёт его. Нужно вручную проверятьisActiveилиensureActive(). job.cancel()— запускает отмену и сразу возвращает управление.job.cancelAndJoin()— ждёт, пока корутина реально завершится.withTimeoutиwithTimeoutOrNull— тоже механизм отмены. По истечении таймаута корутина отменяется черезCancellationException.
Как звучит сильный ответ
«Отмена — это не kill, а просьба. Корутина увидит её только в следующей suspend-точке. Если тяжёлый цикл — ставлю yield() или проверяю isActive на каждой итерации. Если нужно дождаться реальной остановки перед следующим шагом — cancelAndJoin. И помню, что CancellationException — это не ошибка, а нормальное завершения.»
Что объединяет все ошибки
Одна и та же причина: знание API без понимания модели.
Курсы учат синтаксису: «напиши launch», «вызови withContext», «поставь Dispatchers.IO». Но не учат думать в терминах coroutine, не учат том, как распространяются исключения, кто принадлежит к lifecycle. Проверяют не «знаешь ли, что такое SupervisorJob», а «понимаешь ли, зачем он и что сломается, если не поставить».
Что делать прямо сейчас
- Перестань зубрить определения. Вместо «structured concurrency — это...» объясни вслух, что произойдёт в конкретном сценарии.
- Пиши мини-примеры и ломай их. Запусти корутину, отмени её, брось исключение, посмотри, что выживет.
- Проговаривай ответы вслух. Формат «я бы сделал X, потому что Y, а если не так — Z».
- Разбирай реальные вопросы с собеседований. Не абстрактные «расска\", а конкретные: «что вернёт этот код», «где утечка», «как это отменить».
