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. Це контрастує з моделлю «вистрілив і забув», де корутина продовжує жити після завершення 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 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також