Что такое CoroutineScope и почему он важен?
CoroutineScope хранит CoroutineContext и управляет жизненным циклом корутин: при его отмене все дочерние корутины автоматически отменяются. Это основа структурированного параллелизма, защищающая от утечек.
CoroutineScope
CoroutineScope — это контейнер, который хранит CoroutineContext и определяет жизненный цикл корутин. Любая корутина, запущенная через launch или async, привязана к scope: если scope отменяется, все его дочерние корутины тоже отменяются. Это главный механизм предотвращения утечек корутин.
Зачем нужен scope
- Управление жизненным циклом: отмена scope отменяет все запущенные в нём корутины.
- Структурированный параллелизм: scope завершается только тогда, когда завершены все его дочерние корутины.
- Контекст по умолчанию: хранит диспетчер, Job, имя и обработчик исключений для всех дочерних корутин.
Создание собственного scope
import kotlinx.coroutines.*
class DataRepository {
// Собственный scope с SupervisorJob — падение одной задачи не отменяет остальные
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
fun loadData(id: Int) {
scope.launch {
val result = fetchFromNetwork(id) // suspend-функция
println("Loaded: $result")
}
}
// Вызывается при уничтожении объекта
fun cleanup() {
scope.cancel() // отменяет все активные корутины
}
private suspend fun fetchFromNetwork(id: Int): String {
delay(100) // имитация сетевого запроса
return "Data#$id"
}
}
fun main() = runBlocking {
val repo = DataRepository()
repo.loadData(1)
repo.loadData(2)
delay(200)
repo.cleanup()
println("Repository cleaned up")
}
coroutineScope vs CoroutineScope
coroutineScope { } (с маленькой буквы) — это suspend-функция-строитель, которая создаёт дочерний scope и ждёт завершения всех запущенных в нём корутин. Она не требует ручной отмены и автоматически распространяет исключения наверх.
suspend fun loadAll(): Pair<String, String> = coroutineScope {
// оба запроса идут параллельно
val a = async { fetchA() }
val b = async { fetchB() }
a.await() to b.await()
// функция вернётся только когда оба async завершатся
}
suspend fun fetchA(): String { delay(100); return "A" }
suspend fun fetchB(): String { delay(150); return "B" }
GlobalScope — почему его избегают
GlobalScope существует на протяжении всего времени жизни приложения. Корутины, запущенные в нём, не привязаны ни к какому компоненту и не будут отменены при уничтожении экрана или объекта. Это ведёт к утечкам памяти и нежелательным фоновым операциям.
// Плохо — утечка: корутина живёт вечно
GlobalScope.launch {
updateUi() // ViewModel уже уничтожена, а корутина работает
}
// Хорошо — привязано к ViewModel
viewModelScope.launch {
updateUi()
}
Встроенные scope в платформах
viewModelScope(Android) — отменяется при вызовеonCleared().lifecycleScope(Android) — отменяется при уничтожении Lifecycle-владельца.MainScope()— scope сDispatchers.MainиSupervisorJob(), для использования в UI-компонентах.
Подводные камни
- Создание
CoroutineScope(Job())вместоCoroutineScope(SupervisorJob()): падение одной дочерней корутины отменяет весь scope и все остальные задачи. - Забыть вызвать
scope.cancel()вonDestroy/onCleared— корутины продолжат работать после уничтожения компонента. - Использование
GlobalScopeв production-коде ради «удобства» — компилятор не предупреждает, но это источник утечек. - Передача scope как параметра функциям — нарушает инкапсуляцию и делает управление жизненным циклом непредсказуемым. Лучше передавать suspend-функции.
- Путаница между
CoroutineScope()(функция-фабрика, возвращает объект) иcoroutineScope { }(suspend-строитель блока) — разные API с разной семантикой. - Запуск корутины вне scope (через
runBlocking) в production-коде блокирует поток и не участвует в структурированном параллелизме.
Common mistakes
- Объяснять «CoroutineScope» только как синтаксис и не описывать поведение runtime/compiler.
- Игнорировать важный риск: Scope без жизненного цикла превращает фоновые задачи в утечки и теряет исключения.
- Давать пример без edge case: отмены, null, recomposition, platform boundary или ошибки.
What the interviewer is testing
- Формулирует суть темы «CoroutineScope» своими словами и связывает ее с кодом.
- Называет механизм: Builders запускаются как extension functions на scope, поэтому дочерние задачи наследуют контекст и участвуют в structured concurrency.
- Видит production-последствие: Scope без жизненного цикла превращает фоновые задачи в утечки и теряет исключения.