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 — 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.
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.
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.
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.
Zrozumienie wewnętrznej budowy CoroutineScope wymaga znajomości koncepcji Job i zasady konkurencji strukturalnej.
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ą:
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:
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.
Można utworzyć CoroutineScope za pomocą funkcji fabrycznej lub poprzez implementację interfejsu w swojej klasie. Omówimy oba podejścia.
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.
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.
Kotlin umożliwia delegowanie implementacji CoroutineScope za pomocą słowa kluczowego by:
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 — to singleton CoroutineScope dla całej aplikacji. Jego używanie w kodzie produkcyjnym oficjalnie nie jest zalecane.
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()).
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ń.
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.
| Cecha | coroutineScope | supervisorScope |
|---|---|---|
| Zachowanie przy błędzie | Wyjątek w podrzędnej korutynie anuluje wszystkie pozostałe | Wyjątek w podrzędnej korutynie NIE anuluje pozostałych |
| Propagacja błędu | Tak, pierwszy wyjątek jest propagowany na zewnątrz | Tak, pierwszy wyjątek jest propagowany na zewnątrz |
| Domyślny Job | Job() — podrzędne są powiązane z rodzicem | SupervisorJob() — podrzędne nie zależą od siebie |
| Typowy use-case | Operacja atomowa z kilku kroków | Niezależne zadania równoległe (ładowania UI) |
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.
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.
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ć.
Omówimy najczęstsze błędy programistów przy używaniu CoroutineScope w Kotlinie.
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.
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.
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.
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
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.
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.
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.
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.
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
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.
Przeczytaj również