CoroutineScope — co to jest, zakres życia i działanie w korutynach

Autor: IT Sectr Opublikowano: 2026-06-22 Czas czytania: 10 min

CoroutineScope — to interfejs Kotlina, który określa zakres życia korutyny i udostępnia kontekst do uruchamiania nowych korutyn. Według dokumentacji Kotlina, 2025, każda instancja CoroutineScope zawiera CoroutineContext i zarządza wszystkimi uruchomionymi w niej korutynami. Gdy scope zostanie zakończony (cancel), wszystkie podrzędne korutyny są automatycznie anulowane, co zapobiega wyciekom pamięci.

Najważniejsze

  • CoroutineScope — interfejs z jedynym polem CoroutineContext, określający cykl życia korutyn
  • Job — element kontekstu odpowiedzialny za anulowanie: anulowanie scope anuluje wszystkie podrzędne korutyny
  • Konkurencja strukturalna — zasada, w której podrzędne korutyny są powiązane z nadrzędnym scope
  • GlobalScope — scope na całą aplikację, który nie jest zalecany ze względu na ryzyko wycieków
  • supervisorScope — specjalny scope, w którym anulowanie jednej podrzędnej korutyny nie anuluje pozostałych

Co to jest CoroutineScope w Kotlinie?

CoroutineScope — to fundamentalny interfejs z biblioteki kotlinx.coroutines, który służy jako kontener dla korutyn. Określa granice życia korutyn: gdy scope się kończy, wszystkie korutyny wewnątrz niego są automatycznie anulowane.

kotlin
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

Interfejs zawiera tylko jedno pole — coroutineContext. Za jego pośrednictwem scope udostępnia dyspozytora (Dispatcher), zadanie (Job), obsługę wyjątków i inne elementy kontekstu dla wszystkich uruchomionych w nim korutyn.

Rola w bibliotece kotlinx.coroutines

Wszystkie funkcje uruchamiania korutyn — launch, async, runBlocking — są funkcjami rozszerzającymi na CoroutineScope. Oznacza to, że można je wywołać tylko mając obiekt scope. Taki projekt gwarantuje, że każda korutyna ma jasno określonego rodzica i cykl życia.

Gdzie stosuje się CoroutineScope

W Androidzie każdy komponent architektoniczny ma swój scope: viewModelScope dla ViewModel, lifecycleScope dla Activity/Fragment. W aplikacjach serwerowych scope może być powiązany z żądaniem HTTP lub z pulą połączeń z bazą danych.

Jak działa CoroutineScope: Job i konkurencja strukturalna

Zrozumienie wewnętrznej budowy CoroutineScope wymaga znajomości koncepcji Job i zasady konkurencji strukturalnej.

Job — zadanie korutyny

Każda korutyna po uruchomieniu zwraca obiekt Job (lub Deferred dla async). Job reprezentuje zadanie z określonym cyklem życia: New, Active, Completing, Completed, Cancelling, Cancelled. Obiekty Job tworzą strukturę drzewiastą:

  • Nadrzędny Job — scope, w którym uruchomiono korutynę
  • Podrzędny Job — każda korutyna uruchomiona przez launch/async
  • Anulowanie rodzica → anulowanie wszystkich dzieci
  • Wyjątek w dziecku → anulowanie rodzica (z wyjątkiem supervisorScope)

Zasada konkurencji strukturalnej

Konkurencja strukturalna — kluczowa zasada architektoniczna Kotlin Coroutines, w której czas życia korutyny jest powiązany z czasem życia jej scope. Kontrastuje to z modelem „fire-and-forget”, gdzie korutyna żyje dalej po zakończeniu scope. Zalety konkurencji strukturalnej:

  • Przewidywalny cykl życia — gdy scope się kończy, wszystkie korutyny są gwarantowanie zatrzymane
  • Automatyczna obsługa błędów — wyjątek w dowolnej podrzędnej korutynie rozprzestrzenia się na scope
  • Brak wycieków — żadna korutyna nie pozostaje aktywna po zakończeniu scope
  • Jasna hierarchia — kod odzwierciedla logiczną strukturę operacji równoległych

Cykl życia CoroutineScope

Gdy wywoływane jest scope.cancel(), Job scope przechodzi w stan Cancelled, co rekurencyjnie anuluje wszystkie podrzędne Job. Po anulowaniu scope może być ponownie użyty tylko poprzez utworzenie nowej instancji CoroutineScope.

Tworzenie i konfiguracja CoroutineScope

Można utworzyć CoroutineScope za pomocą funkcji fabrycznej lub poprzez implementację interfejsu w swojej klasie. Omówimy oba podejścia.

Funkcja fabryczna CoroutineScope()

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

scope.launch {
    println("Uruchomiono na ${Thread.currentThread().name}")
}

Funkcja fabryczna przyjmuje CoroutineContext i tworzy scope z określonym kontekstem. W przykładzie użyto Dispatchers.Default dla zadań CPU-intensywnych oraz SupervisorJob, który izoluje wyjątki między podrzędnymi korutynami.

Implementacja interfejsu przez kompozycję

kotlin
class MyRepository {
    private val scope = CoroutineScope(Dispatchers.IO + Job())

    suspend fun fetchData(): Data = scope.async {
        api.getData()
    }.await()

    fun cleanup() {
        scope.cancel()
    }
}

Przechowujemy scope jako pole klasy i ręcznie wywołujemy cleanup, aby go anulować. Jest to odpowiednie dla komponentów z zarządzanym cyklem życia — na przykład dla repozytoriów lub menedżerów.

Implementacja przez delegację

Kotlin umożliwia delegowanie implementacji CoroutineScope za pomocą słowa kluczowego by:

kotlin
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
    fun load() {
        launch {
            // korutyna działa w zakresie DataLoader
        }
    }
}

Takie podejście jest wygodne, gdy klasa sama jest scope i chce udostępniać metody uruchamiania korutyn. Należy jednak zachować ostrożność: klasa dziedziczy wszystkie metody CoroutineScope, w tym cancel, co może naruszyć enkapsulację.

GlobalScope a własny CoroutineScope

GlobalScope — to singleton CoroutineScope dla całej aplikacji. Jego używanie w kodzie produkcyjnym oficjalnie nie jest zalecane.

Problemy GlobalScope

  • Brak konkurencji strukturalnej — korutyny w GlobalScope nie są powiązane z cyklem życia komponentu
  • Wycieki pamięci — korutyna może kontynuować działanie po zamknięciu Activity/Fragment
  • Utrudnione testowanie — GlobalScope nie można zastąpić w testach
  • Niekontrolowane zużycie zasobów — wiele korutyn może działać dłużej niż oczekiwano

Kiedy GlobalScope jest uzasadniony

JetBrains dopuszcza GlobalScope tylko w rzadkich scenariuszach: procesy tła na poziomie aplikacji, które powinny działać nawet po zamknięciu wszystkich Activity (na przykład synchronizacja danych, analityka). Ale nawet w tych przypadkach lepiej jest utworzyć własny scope z CoroutineScope(SupervisorJob()).

Zalecenie

Zawsze używaj własnego CoroutineScope z jawnym zarządzaniem cyklem życia. W Androidzie są to viewModelScope i lifecycleScope. W aplikacjach serwerowych twórz scope dla każdego żądania lub puli połączeń.

coroutineScope vs supervisorScope: jaka jest różnica

Obie funkcje są funkcjami suspend, które tworzą tymczasowy scope dla zadań równoległych, ale ich zachowanie przy wyjątkach zasadniczo się różni.

CechacoroutineScopesupervisorScope
Zachowanie przy błędzieWyjątek w podrzędnej korutynie anuluje wszystkie pozostałeWyjątek w podrzędnej korutynie NIE anuluje pozostałych
Propagacja błęduTak, pierwszy wyjątek jest propagowany na zewnątrzTak, pierwszy wyjątek jest propagowany na zewnątrz
Domyślny JobJob() — podrzędne są powiązane z rodzicemSupervisorJob() — podrzędne nie zależą od siebie
Typowy use-caseOperacja atomowa z kilku krokówNiezależne zadania równoległe (ładowania UI)

Kiedy wybrać coroutineScope

Używaj coroutineScope, gdy kilka równoległych operacji tworzy jedną atomową operację. Na przykład ładowanie danych z trzech serwerów: jeśli jedno żądanie upadnie, pozostałe nie mają sensu.

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

Jeśli getProduct lub getReviews wyrzucają wyjątek — obie korutyny są anulowane, a wyjątek jest propagowany do kodu wywołującego.

Kiedy wybrać supervisorScope

Używaj supervisorScope, gdy operacje równoległe nie zależą od siebie. Na przykład ładowanie danych profilu w kilku niezależnych sekcjach: jeśli sekcja rekomendacji upadnie, nagłówek profilu i lista znajomych powinny się wyświetlić.

Typowe błędy przy pracy z CoroutineScope

Omówimy najczęstsze błędy programistów przy używaniu CoroutineScope w Kotlinie.

Błąd 1: Zapomniano anulować scope

Najczęstszy scenariusz wycieku korutyny — utworzenie scope bez wywołania cancel przy zakończeniu komponentu. Jeśli scope nie został anulowany, korutyny nadal działają, przechowując referencje do obiektów. W Androidzie używaj viewModelScope lub lifecycleScope, które są anulowane automatycznie.

Błąd 2: Używanie GlobalScope w Activity lub Fragment

GlobalScope ignoruje cykl życia komponentów Androida. Korutyna uruchomiona w GlobalScope po zamknięciu Activity będzie kontynuować działanie i próbować zaktualizować UI — co doprowadzi do crasha. Zawsze używaj lifecycleScope dla komponentów UI.

Błąd 3: Ponowne użycie anulowanego scope

Po wywołaniu cancel() scope nie może być ponownie użyty — wszystkie korutyny w nim są już zakończone. Utwórz nową instancję CoroutineScope przez funkcję fabryczną. Job() nie obsługuje ponownej aktywacji.

Błąd 4: Nieprawidłowa delegacja interfejsu CoroutineScope

Przy delegacji przez by klasa otrzymuje publiczną metodę cancel(), która może być wywołana z dowolnego miejsca, naruszając enkapsulację. Przechowuj scope jako prywatne pole, a nie deleguj interfejs.

Często zadawane pytania

Czym różni się CoroutineScope od CoroutineContext?

CoroutineScope — to interfejs posiadający CoroutineContext i odpowiedzialny za cykl życia korutyn. CoroutineContext — to zbiór elementów (dyspozytor, job, obsługa błędów), określający „jak” wykonuje się korutyna. Jedna z różnic: scope tworzy korutyny, context zarządza ich zachowaniem.

Czy można utworzyć CoroutineScope z SupervisorJob?

Tak, to standardowy wzorzec: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob zapobiega kaskadowemu anulowaniu podrzędnych korutyn przy wyjątku w jednej z nich. Jest to przydatne w niezależnych zadaniach równoległych, gdzie błąd w jednym nie powinien zatrzymywać innych.

Ile korutyn może zawierać CoroutineScope?

Nie ma ograniczeń co do liczby korutyn w scope — są one ograniczone tylko dostępną pamięcią i ustawieniami dyspozytora. Praktyczny limit wynosi zwykle tysiące aktywnych korutyn w jednym scope. Jednak duża liczba korutyn może wskazywać na problemy architektoniczne.

Jak przetestować kod z CoroutineScope?

Prawidłowy sposób to przekazywanie scope do klasy przez konstruktor lub użycie runBlockingTest / runTest z kotlinx-coroutines-test. W testach można zastąpić scope na TestCoroutineDispatcher i ręcznie kontrolować wykonywanie korutyn.

Czy jedna korutyna może mieć swój własny scope?

Nie, scope to zewnętrzny kontener dla korutyny. Sama korutyna nie jest scope. Jednak wewnątrz korutyny można utworzyć nowy scope przez coroutineScope lub supervisorScope do równoległego uruchamiania podrzędnych korutyn.

Podsumowanie

  • CoroutineScope — interfejs z polem coroutineContext, określający cykl życia uruchomionych w nim korutyn
  • Konkurencja strukturalna — anulowanie scope automatycznie anuluje wszystkie podrzędne korutyny, zapobiegając wyciekom pamięci
  • Job i SupervisorJob — dwa tryby obsługi błędów: kaskadowe anulowanie (Job) i izolowane błędy (SupervisorJob)
  • GlobalScope — niezalecany w produkcji z powodu braku powiązania z cyklem życia
  • coroutineScope vs supervisorScope — atomowe operacje równoległe przeciwko niezależnym zadaniom równoległym
  • viewModelScope i lifecycleScope — gotowe scope dla Androida, anulowane automatycznie przy zakończeniu komponentu
  • Funkcja fabryczna — preferowany sposób tworzenia scope przez CoroutineContext + jawne wywołanie cancel

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również