withContext — to funkcja przełączająca kontekst wykonania wewnątrz współprogramu, która tymczasowo zmienia wątek lub dyspozytora dla określonego bloku kodu i zwraca wynik z powrotem do pierwotnego kontekstu. Według danych JetBrains, 2025, withContext jest jednym z najczęściej używanych narzędzi korutyn do pracy z żądaniami sieciowymi i operacjami dyskowymi. Funkcja gwarantuje, że po zakończeniu bloku korutyna kontynuuje działanie na pierwotnym dyspozytorze, co zapobiega przypadkowym błędom bezpieczeństwa wątkowego.
Najważniejsze
withContext — to funkcja zawieszająca z pakietu kotlinx.coroutines, która wykonuje przekazany blok kodu w zadanym CoroutineContext i zwraca wynik z powrotem do pierwotnego kontekstu. Sygnatura funkcji wygląda następująco:
suspend fun withContext (
context: CoroutineContext,
block: suspend CoroutineScope.() -> T
): T
Parametr context przyjmuje dowolny CoroutineContext — najczęściej jeden ze standardowych Dispatchers.IO, Dispatchers.Default lub Dispatchers.Main. Blok wykonuje się właśnie w tym kontekście, a wynik jest zwracany tam, skąd został wywołany withContext.
Po zakończeniu lambdy withContext gwarantowanie przełącza wykonanie z powrotem na pierwotny dyspozytor. Oznacza to, że programista nie musi ręcznie wywoływać withContext(Dispatchers.Main) po operacji w tle — powrót następuje automatycznie. Takie zachowanie zostało ustalone w specyfikacji Kotlin Coroutines od wersji 1.3.
Tworzenie aplikacji na Androida — główny obszar zastosowania withContext. Typowy scenariusz: ViewModel uruchamia korutynę na głównym wątku, wewnątrz wywoływany jest withContext(Dispatchers.IO) dla żądania sieciowego, a wynik po automatycznym powrocie do Main jest używany do aktualizacji UI. Takie podejście leży u podstaw architektury MVVM i jest zalecane przez Google w oficjalnym przewodniku po korutynach.
Aby zrozumieć withContext, trzeba poznać CoroutineContext i jego kluczowy komponent — dyspozytora (Dispatcher). Każda współprograma ma zestaw elementów kontekstu, wśród których dyspozytor określa, na którym wątku lub puli wątków wykonywany jest kod.
| Dyspozytor | Przeznaczenie | Rozmiar puli |
|---|---|---|
| Dispatchers.Main | Główny wątek UI (Android, JavaFX, Swing) | 1 (główny wątek) |
| Dispatchers.IO | Operacje dyskowe i sieciowe | 64 wątki (limit rośnie) |
| Dispatchers.Default | Obliczenia CPU-intensywne | max(2, liczba rdzeni) |
| Dispatchers.Unconfined | Bez stałego wątku | nieograniczony |
Ważne jest, aby zrozumieć, że withContext nie tworzy nowej korutyny — on tylko przełącza kontekst dla istniejącej. To kluczowa różnica w stosunku do launch i async, które tworzą nowe współprogramy. Wewnętrzna implementacja withContext jest zoptymalizowana: jeśli żądany kontekst jest zgodny z bieżącym, przełączenie nie następuje — funkcja wykonuje się na tym samym dyspozytorze.
Dispatchers.Main wewnątrz withContext(Dispatchers.Main) nie powoduje przełączenia — Kotlin Coroutines rozpoznaje identyczność kontekstów i pomija zbędną operację. Podobnie withContext(Dispatchers.Default) wewnątrz korutyny już działającej na Default nie generuje narzutu. Ta optymalizacja jest zaimplementowana w ContinuationInterceptor.
Początkujący często mylą withContext z launch i async, ponieważ wszystkie trzy funkcje pracują z korutynami i kontekstem. Jednak ich przeznaczenie zasadniczo się różni.
| Cecha | withContext | launch | async |
|---|---|---|---|
| Tworzy nową korutynę | Nie | Tak | Tak |
| Zwraca wynik | Tak (T bezpośrednio) | Nie (Job) | Tak (Deferred<T>) |
| Wykonanie | Sekwencyjne | Równoległe | Równoległe |
| Oczekiwanie na wynik | Automatyczne | join() | await() |
| Typowy use-case | Zmiana dyspozytora | Fire-and-forget | Obliczenia równoległe |
Jeśli trzeba wykonać jedną operację w wątku tła i uzyskać wynik — użyj withContext. Jeśli trzeba uruchomić kilka niezależnych operacji równolegle — użyj async z await. Jeśli wynik nie jest potrzebny (logowanie, zapis cache) — launch. Google zaleca withContext jako preferowane narzędzie dla warstwy Repository w architekturze Androida.
Rozważmy trzy praktyczne scenariusze użycia withContext w aplikacjach na Androida w Kotlinie. Każdy przykład demonstruje konkretne zadanie i prawidłowy wzorzec.
ViewModel wywołuje metodę repozytorium z korutyny na Main. Wewnątrz withContext(Dispatchers.IO) wykonywane jest żądanie HTTP, a wynik jest zwracany automatycznie:
class UserRepository(
private val api: UserApi
) {
suspend fun getUser(id: String): User {
return withContext(Dispatchers.IO) {
api.fetchUser(id)
}
}
}
Korutyna w ViewModel wywołuje getUser tak samo jak zwykłą funkcję zawieszającą — bez jawnego określania dyspozytora. withContext ukrywa szczegóły przełączania wątków.
Gdy trzeba wykonać kilka operacji IO jedna po drugiej, withContext łączy je w jeden blok. Jest to bardziej efektywne niż owijanie każdej operacji w osobny withContext:
suspend fun loadUserProfile(id: String): Profile {
return withContext(Dispatchers.IO) {
val user = api.fetchUser(id)
val posts = api.fetchPosts(id)
Profile(user, posts)
}
}
Obie operacje wykonują się na Dispatchers.IO, a wynik Profile jest tworzony i zwracany bez zbędnych przełączeń kontekstu. Jeśli operacje są niezależne, lepiej użyć async do równoległego wykonania.
W niektórych scenariuszach trzeba wykonać kod, którego nie można anulować — na przykład zapis stanu przy zamykaniu ekranu. Kombinacja withContext + NonCancellable rozwiązuje to zadanie:
withContext(Dispatchers.IO + NonCancellable) {
cache.saveState(state)
analytics.logEvent("state_saved")
}
Operator + łączy dwa elementy kontekstu: dyspozytor IO i flagę NonCancellable. Blok wykonuje się nawet jeśli nadrzędna korutyna została anulowana — jest to przydatne dla operacji finalizujących.
Wewnętrzna implementacja withContext opiera się na mechanizmie Continuation — centralnej abstrakcji korutyn Kotlina. Każdy punkt zawieszenia (suspend point) zapisuje stan wykonania w obiekcie Continuation i withContext nie jest wyjątkiem.
Kompilator Kotlina transluje withContext na wywołanie metody withContext z kotlinx.coroutines, która wewnętrznie tworzy nową instancję DispatchedContinuation. Ten obiekt opakowuje oryginalny Continuation i zastępuje w nim dyspozytora. Jeśli nowy dyspozytor różni się od bieżącego, wykonanie jest zawieszane, blok wysyłany do odpowiedniej puli wątków, a po zakończeniu — wznawiane z pierwotnym kontekstem.
Gdy withContext jest wywoływany z tym samym dyspozytorem, na którym już działa korutyna, Kotlin uruchamia fast-path: blok wykonuje się synchronicznie, bez tworzenia DispatchedContinuation i bez przekazywania do puli wątków. To sprawia, że withContext jest praktycznie darmowy przy ponownych wywołaniach z tym samym kontekstem. Według benchmarków JetBrains (kotlinx.coroutines 1.8), fast-path wykonuje się poniżej 0,1 μs.
Każde wywołanie withContext z innym dyspozytorem tworzy nowy DispatchedContinuation i wymaga przełączenia wątków — zajmuje to od 1 do 5 μs w zależności od obciążenia. Dla większości aplikacji takie opóźnienie jest niezauważalne, ale w pętlach z tysiącami iteracji warto agregować operacje w jeden blok withContext.
Nawet doświadczeni programiści popełniają błędy podczas pracy z withContext. Rozważmy cztery najczęstsze problemy i sposoby ich zapobiegania.
Programiści często owijają każdą linię w osobny withContext, zamiast połączyć operacje w jeden blok. Każde zbędne wywołanie z innym dyspozytorem generuje narzut.
Prawidłowo: połączyć sekwencyjne operacje IO w jeden withContext(Dispatchers.IO) { ... }. Jeśli część operacji jest CPU-intensywna — użyj withContext(Dispatchers.Default) wewnątrz tego samego bloku.
withContext wykonuje kod sekwencyjnie. Jeśli dwa niezależne żądania sieciowe są owinięte w jeden withContext, będą wykonywane jedno po drugim. Dla równoległości używaj async + await.
// Sekwencyjnie — wolno
withContext(Dispatchers.IO) {
val a = api.fetchA()
val b = api.fetchB()
}
// Równolegle — szybko
coroutineScope {
val a = async { api.fetchA() }
val b = async { api.fetchB() }
println("${a.await()} ${b.await()}")
}
Jeśli korutyna zostanie anulowana podczas withContext, blok na Dispatchers.IO również zostanie przerwany. Dla operacji, które muszą się zakończyć za wszelką cenę (zapis do bazy danych, wysłanie analityki), łącz withContext z NonCancellable.
Nigdy nie aktualizuj komponentów View wewnątrz withContext(Dispatchers.IO). withContext nie wraca do Main aż do zakończenia całego bloku. Aktualizację UI wykonuj po zamykającym nawiasie withContext — wtedy korutyna będzie już na głównym wątku.
Często zadawane pytania
withContext — funkcja zawieszająca, która nie blokuje wątku, a przełącza kontekst wewnątrz istniejącej korutyny. runBlocking — most między korutynami a zwykłym kodem, który blokuje bieżący wątek do zakończenia. withContext jest bezpieczny dla wątku UI, runBlocking — nie.
Nie, withContext to funkcja zawieszająca, więc można ją wywołać tylko z innej funkcji zawieszającej lub z korutyny (launch/async). Ze zwykłej funkcji withContext nie jest wywoływany — do tego potrzebny jest runBlocking lub CoroutineScope.
Kotlin aktywuje fast-path — blok wykonuje się synchronicznie na tym samym wątku bez przełączania. Narzut wynosi poniżej 0,1 μs. Nie jest to błąd, ale takie wywołanie jest zbędne — lepiej po prostu wykonać kod bez withContext.
Wyjątki wewnątrz withContext są propagowane tak samo jak w zwykłym kodzie — przez try-catch. Jeśli blok rzucił wyjątek, rozprzestrzenia się on do nadrzędnej korutyny i anuluje ją, jeśli nie został obsłużony. Użyj try-catch wewnątrz withContext lub wokół niego.
Nie, withContext nie tworzy nowej korutyny. Używa istniejącego współprogramu, ale tymczasowo zmienia jego kontekst. To odróżnia go od launch i async, które tworzą korutyny potomne. Zachowanie jest potwierdzone w kodzie źródłowym kotlinx.coroutines.
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ż