CoroutineScope — это интерфейс Kotlin, который определяет область жизни сопрограммы и предоставляет контекст для запуска новых корутин. По данным документации Kotlin, 2025, каждый экземпляр CoroutineScope содержит CoroutineContext и управляет всеми запущенными в нём корутинами. Когда scope завершается (cancel), все дочерние корутины автоматически отменяются, что предотвращает утечки памяти.
Главное
CoroutineScope — это фундаментальный интерфейс из библиотеки kotlinx.coroutines, который служит контейнером для корутин. Он определяет границы жизни сопрограмм: когда scope завершается, все корутины внутри него автоматически отменяются.
public interface CoroutineScope {
public val coroutineContext: CoroutineContext
}
Интерфейс содержит всего одно поле — coroutineContext. Через него scope предоставляет диспетчер (Dispatcher), задачу (Job), обработчик исключений и другие элементы контекста для всех запущенных в нём корутин.
Все функции запуска корутин — launch, async, runBlocking — являются extension-функциями на CoroutineScope. Это означает, что вызвать их можно только при наличии объекта scope. Такой дизайн гарантирует, что каждая корутина имеет чётко определённого родителя и жизненный цикл.
В Android каждый архитектурный компонент имеет свой scope: viewModelScope для ViewModel, lifecycleScope для Activity/Fragment. В серверных приложениях scope может быть привязан к HTTP-запросу или к пулу соединений с базой данных.
Понимание внутреннего устройства CoroutineScope требует знакомства с концепцией Job и принципом структурной конкуренции.
Каждая корутина при запуске возвращает объект Job (или Deferred для async). Job представляет собой задачу с конечным жизненным циклом: New, Active, Completing, Completed, Cancelling, Cancelled. Job-объекты образуют древовидную структуру:
Структурная конкуренция — ключевой архитектурный принцип Kotlin Coroutines, при котором время жизни корутины привязано к времени жизни её scope. Это контрастирует с моделью «fire-and-forget», где корутина продолжает жить после завершения scope. Преимущества структурной конкуренции:
Когда вызывается scope.cancel(), Job scope переходит в состояние Cancelled, что рекурсивно отменяет все дочерние Job. После отмены scope может быть переиспользован только если создать новый экземпляр CoroutineScope.
Создать CoroutineScope можно через фабричную функцию или через имплементацию интерфейса в своём классе. Рассмотрим оба подхода.
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
scope.launch {
println("Running on ${Thread.currentThread().name}")
}
Фабричная функция принимает CoroutineContext и создаёт scope с указанным контекстом. В примере используется Dispatchers.Default для CPU-интенсивных задач и SupervisorJob, который изолирует исключения между дочерними корутинами.
class MyRepository {
private val scope = CoroutineScope(Dispatchers.IO + Job())
suspend fun fetchData(): Data = scope.async {
api.getData()
}.await()
fun cleanup() {
scope.cancel()
}
}
Мы храним scope как поле класса и вручную вызываем cleanup для его отмены. Это подходит для компонентов с управляемым жизненным циклом — например, для репозиториев или менеджеров.
Kotlin позволяет делегировать имплементацию CoroutineScope через ключевое слово by:
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
fun load() {
launch {
// coroutine runs in DataLoader scope
}
}
}
Такой подход удобен, когда класс сам является scope и хочет предоставлять методы запуска корутин. Однако будьте осторожны: класс наследует все методы CoroutineScope, включая cancel, что может нарушить инкапсуляцию.
GlobalScope — это синглтон CoroutineScope для всего приложения. Его использование в production-коде официально не рекомендуется.
JetBrains допускает GlobalScope только в редких сценариях: фоновые процессы на уровне приложения, которые должны жить даже после закрытия всех Activity (например, синхронизация данных, аналитика). Но и в этих случаях предпочтительнее создать собственный scope с CoroutineScope(SupervisorJob()).
Всегда используйте кастомный CoroutineScope с явным управлением жизненным циклом. В Android это viewModelScope и lifecycleScope. В серверных приложениях создавайте scope для каждого запроса или пула соединений.
Обе функции являются suspend-функциями, которые создают временный scope для параллельных задач, но их поведение при исключениях принципиально различается.
| Характеристика | coroutineScope | supervisorScope |
|---|---|---|
| Поведение при ошибке | Исключение в дочерней корутине отменяет все остальные | Исключение в дочерней корутине НЕ отменяет остальные |
| Проброс ошибки | Да, первое исключение пробрасывается наружу | Да, первое исключение пробрасывается наружу |
| Job по умолчанию | Job() — дочерние привязаны к родителю | SupervisorJob() — дочери не зависят друг от друга |
| Типичный use-case | Атомарная операция из нескольких шагов | Независимые параллельные задачи (UI-загрузки) |
Используйте coroutineScope, когда несколько параллельных операций образуют единую атомарную операцию. Например, загрузка данных с трёх серверов: если один запрос упал, остальные не имеют смысла.
suspend fun loadProductPage(): ProductPage = coroutineScope {
val product = async { api.getProduct() }
val reviews = async { api.getReviews() }
ProductPage(product.await(), reviews.await())
}
Если getProduct или getReviews выбрасывают исключение — обе корутины отменяются, а исключение пробрасывается вызывающему коду.
Используйте supervisorScope, когда параллельные операции не зависят друг от друга. Например, загрузка данных профиля в нескольких независимых секциях: если секция рекомендаций упала, заголовок профиля и список друзей должны отобразиться.
Рассмотрим наиболее частые ошибки разработчиков при использовании CoroutineScope в Kotlin.
Самый распространённый сценарий утечки корутины — создание scope без вызова cancel при завершении компонента. Если scope не отменён, корутины продолжают работать, удерживая ссылки на объекты. В Android используйте viewModelScope или lifecycleScope, которые отменяются автоматически.
GlobalScope игнорирует жизненный цикл Android-компонентов. Корутина, запущенная в GlobalScope после закрытия Activity, продолжит выполнение и попытается обновить UI — что приведёт к краху. Всегда используйте lifecycleScope для UI-компонентов.
После вызова cancel() scope нельзя переиспользовать — все корутины в нём уже завершены. Создайте новый экземпляр CoroutineScope через фабричную функцию. Job() не поддерживает повторную активацию.
При делегировании by класс получает публичный метод cancel(), который может быть вызван из любого места, нарушив инкапсуляцию. Храните scope приватным полем, а не делегируйте интерфейс.
Часто задаваемые вопросы
CoroutineScope — это интерфейс, владеющий CoroutineContext и отвечающий за жизненный цикл корутин. CoroutineContext — это набор элементов (диспетчер, job, обработчик ошибок), определяющий «как» выполняется корутина. Одно из различий: scope создаёт корутины, context управляет их поведением.
Да, это стандартный паттерн: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob предотвращает каскадную отмену дочерних корутин при исключении в одной из них. Это полезно для независимых параллельных задач, где ошибка в одной не должна останавливать другие.
Ограничений на количество корутин в scope нет — они ограничиваются только доступной памятью и настройками диспетчера. Практический лимит обычно составляет тысячи активных корутин в одном scope. Однако большое количество корутин может указывать на архитектурные проблемы.
Правильный способ — передавать scope в класс через конструктор или использовать runBlockingTest / runTest из kotlinx-coroutines-test. В тестах можно заменить scope на TestCoroutineDispatcher и контролировать выполнение корутин вручную.
Нет, scope — это внешний контейнер для корутины. Сама корутина не является scope. Однако внутри корутины можно создать новый scope через coroutineScope или supervisorScope для параллельного запуска дочерних корутин.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также