CoroutineScope — що це, життєвий цикл та робота в корутинах

Автор: IT Sectr Опубліковано: 2026-06-22 Час читання: 10 хв

CoroutineScope — це інтерфейс Kotlin, який визначає область життя співпрограми та надає контекст для запуску нових корутин. За даними документації Kotlin, 2025, кожен екземпляр CoroutineScope містить CoroutineContext та керує всіма запущеними в ньому корутинами. Коли scope завершується (cancel), всі дочірні корутини автоматично скасовуються, що запобігає витокам пам'яті.

Головне

  • CoroutineScope — інтерфейс з єдиним полем CoroutineContext, що визначає життєвий цикл корутин
  • Job — елемент контексту, що відповідає за скасування: скасування scope скасовує всі дочірні корутини
  • Структурна конкуренція — принцип, за якого дочірні корутини прив'язані до батьківського scope
  • GlobalScope — scope на весь застосунок, який не рекомендується використовувати через ризик витоків
  • supervisorScope — спеціальний scope, в якому скасування однієї дочірньої корутини не скасовує інші

Що таке CoroutineScope в Kotlin?

CoroutineScope — це фундаментальний інтерфейс з бібліотеки kotlinx.coroutines, який служить контейнером для корутин. Він визначає межі життя співпрограм: коли scope завершується, всі корутини всередині нього автоматично скасовуються.

kotlin
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Інтерфейс містить лише одне поле — coroutineContext. Через нього scope надає диспетчер (Dispatcher), задачу (Job), обробник винятків та інші елементи контексту для всіх запущених в ньому корутин.

Роль в бібліотеці kotlinx.coroutines

Всі функції запуску корутин — launch, async, runBlocking — є extension-функціями на CoroutineScope. Це означає, що викликати їх можна лише за наявності об'єкта scope. Такий дизайн гарантує, що кожна корутина має чітко визначеного батька та життєвий цикл.

Де застосовується CoroutineScope

В Android кожен архітектурний компонент має свій scope: viewModelScope для ViewModel, lifecycleScope для Activity/Fragment. В серверних застосунках scope може бути прив'язаний до HTTP-запиту або до пулу з'єднань з базою даних.

Як працює CoroutineScope: Job і структурна конкуренція

Розуміння внутрішнього устрою CoroutineScope вимагає знайомства з концепцією Job та принципом структурної конкуренції.

Job — задача корутини

Кожна корутина при запуску повертає об'єкт Job (або Deferred для async). Job являє собою задачу з кінцевим життєвим циклом: New, Active, Completing, Completed, Cancelling, Cancelled. Job-об'єкти утворюють деревоподібну структуру:

  • Батьківський Job — scope, в якому запущена корутина
  • Дочірній Job — кожна корутина, запущена через launch/async
  • Скасування батька → скасування всіх дітей
  • Виняток в дитині → скасування батька (крім supervisorScope)

Принцип структурної конкуренції

Структурна конкуренція — ключовий архітектурний принцип Kotlin Coroutines, за якого час життя корутини прив'язаний до часу життя її scope. Це контрастує з моделлю «вистрілив і забув», де корутина продовжує жити після завершення scope. Переваги структурної конкуренції:

  • Прогнозований життєвий цикл — коли scope завершується, всі корутини гарантовано зупинені
  • Автоматична обробка помилок — виняток в будь-якій дочірній корутині поширюється на scope
  • Відсутність витоків — жодна корутина не залишається висіти після завершення scope
  • Чітка ієрархія — код відображає логічну структуру паралельних операцій

Життєвий цикл CoroutineScope

Коли викликається scope.cancel(), Job scope переходить в стан Cancelled, що рекурсивно скасовує всі дочірні Job. Після скасування scope може бути перевикористаний лише якщо створити новий екземпляр CoroutineScope.

Створення та налаштування CoroutineScope

Створити CoroutineScope можна через фабричну функцію або через імплементацію інтерфейсу в своєму класі. Розглянемо обидва підходи.

Фабрична функція CoroutineScope()

kotlin
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

scope.launch {
    println("Running on ${Thread.currentThread().name}")
}

Фабрична функція приймає CoroutineContext і створює scope з вказаним контекстом. В прикладі використовується Dispatchers.Default для CPU-інтенсивних задач та SupervisorJob, який ізолює винятки між дочірніми корутинами.

Імплементація інтерфейсу через композицію

kotlin
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:

kotlin
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
    fun load() {
        launch {
            // coroutine runs in DataLoader scope
        }
    }
}

Такий підхід зручний, коли клас сам є scope і хоче надавати методи запуску корутин. Однак будьте обережні: клас успадковує всі методи CoroutineScope, включаючи cancel, що може порушити інкапсуляцію.

GlobalScope проти кастомного CoroutineScope

GlobalScope — це синглтон CoroutineScope для всього застосунку. Його використання в production-коді офіційно не рекомендується.

Проблеми GlobalScope

  • Відсутність структурної конкуренції — корутини в GlobalScope не прив'язані до життєвого циклу компонента
  • Витоки пам'яті — корутина може продовжувати виконуватися після закриття Activity/Fragment
  • Утруднене тестування — GlobalScope не можна замінити в тестах
  • Неконтрольоване споживання ресурсів — безліч корутин можуть працювати довше, ніж очікується

Коли GlobalScope виправданий

JetBrains допускає GlobalScope лише в рідкісних сценаріях: фонові процеси на рівні застосунку, які повинні жити навіть після закриття всіх Activity (наприклад, синхронізація даних, аналітика). Але і в цих випадках краще створити власний scope з CoroutineScope(SupervisorJob()).

Рекомендація

Завжди використовуйте кастомний CoroutineScope з явним управлінням життєвим циклом. В Android це viewModelScope та lifecycleScope. В серверних застосунках створюйте scope для кожного запиту або пулу з'єднань.

coroutineScope vs supervisorScope: в чому різниця

Обидві функції є suspend-функціями, які створюють тимчасовий scope для паралельних задач, але їх поведінка при винятках принципово різниться.

ХарактеристикаcoroutineScopesupervisorScope
Поведінка при помилціВиняток в дочірній корутині скасовує всі іншіВиняток в дочірній корутині НЕ скасовує інші
Проброс помилкиТак, перший виняток пробрасывается назовніТак, перший виняток пробрасывается назовні
Job за замовчуваннямJob() — дочірні прив'язані до батькаSupervisorJob() — дочірні не залежать один від одного
Типовий use-caseАтомарна операція з кількох кроківНезалежні паралельні задачі (UI-завантаження)

Коли вибирати coroutineScope

Використовуйте coroutineScope, коли кілька паралельних операцій утворюють єдину атомарну операцію. Наприклад, завантаження даних з трьох серверів: якщо один запит впав, інші не мають сенсу.

kotlin
suspend fun loadProductPage(): ProductPage = coroutineScope {
    val product = async { api.getProduct() }
    val reviews = async { api.getReviews() }
    ProductPage(product.await(), reviews.await())
}

Якщо getProduct або getReviews викидають виняток — обидві корутини скасовуються, а виняток пробрасывается коду, що викликає.

Коли вибирати supervisorScope

Використовуйте supervisorScope, коли паралельні операції не залежать одна від одної. Наприклад, завантаження даних профілю в кількох незалежних секціях: якщо секція рекомендацій впала, заголовок профілю та список друзів повинні відобразитися.

Типові помилки при роботі з CoroutineScope

Розглянемо найбільш часті помилки розробників при використанні CoroutineScope в Kotlin.

Помилка 1: Забули скасувати scope

Найпоширеніший сценарій витоку корутини — створення scope без виклику cancel при завершенні компонента. Якщо scope не скасовано, корутини продовжують працювати, утримуючи посилання на об'єкти. В Android використовуйте viewModelScope або lifecycleScope, які скасовуються автоматично.

Помилка 2: Використання GlobalScope в Activity або Fragment

GlobalScope ігнорує життєвий цикл Android-компонентів. Корутина, запущена в GlobalScope після закриття Activity, продовжить виконання і спробує оновити UI — що призведе до краху. Завжди використовуйте lifecycleScope для UI-компонентів.

Помилка 3: Перевикористання скасованого scope

Після виклику cancel() scope не можна перевикористовувати — всі корутини в ньому вже завершені. Створіть новий екземпляр CoroutineScope через фабричну функцію. Job() не підтримує повторну активацію.

Помилка 4: Неправильне делегування інтерфейсу CoroutineScope

При делегуванні by клас отримує публічний метод cancel(), який може бути викликаний з будь-якого місця, порушивши інкапсуляцію. Зберігайте scope приватним полем, а не делегуйте інтерфейс.

Часто задавані питання

Чим відрізняється CoroutineScope від CoroutineContext?

CoroutineScope — це інтерфейс, який володіє CoroutineContext та відповідає за життєвий цикл корутин. CoroutineContext — це набір елементів (диспетчер, job, обробник помилок), що визначає «як» виконується корутина. Одна з відмінностей: scope створює корутини, context керує їх поведінкою.

Чи можна створити CoroutineScope з SupervisorJob?

Так, це стандартний патерн: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob запобігає каскадному скасуванню дочірніх корутин при винятку в одній з них. Це корисно для незалежних паралельних задач, де помилка в одній не повинна зупиняти інші.

Скільки корутин може містити CoroutineScope?

Обмежень на кількість корутин в scope немає — вони обмежуються лише доступною пам'яттю та налаштуваннями диспетчера. Практичний ліміт зазвичай становить тисячі активних корутин в одному scope. Однак велика кількість корутин може вказувати на архітектурні проблеми.

Як протестувати код з CoroutineScope?

Правильний спосіб — передавати scope в клас через конструктор або використовувати runBlockingTest / runTest з kotlinx-coroutines-test. В тестах можна замінити scope на TestCoroutineDispatcher та контролювати виконання корутин вручну.

Чи може одна корутина мати свій власний scope?

Ні, scope — це зовнішній контейнер для корутини. Сама корутина не є scope. Однак всередині корутини можна створити новий scope через coroutineScope або supervisorScope для паралельного запуску дочірніх корутин.

Підсумки

  • CoroutineScope — інтерфейс з полем coroutineContext, що визначає життєвий цикл запущених в ньому співпрограм
  • Структурна конкуренція — скасування scope автоматично скасовує всі дочірні корутини, запобігаючи витокам пам'яті
  • Job і SupervisorJob — два режими обробки помилок: каскадне скасування (Job) та ізольовані помилки (SupervisorJob)
  • GlobalScope — не рекомендується для production через відсутність прив'язки до життєвого циклу
  • coroutineScope vs supervisorScope — атомарні паралельні операції проти незалежних паралельних задач
  • viewModelScope та lifecycleScope — готові scope для Android, що скасовуються автоматично при завершенні компонента
  • Фабрична функція — найкращий спосіб створення scope через CoroutineContext + явний виклик cancel

Ми розробимо мобільний застосунок під ключ

IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.

Обговорити проект

Читайте також