viewModelScope — to wbudowany CoroutineScope z biblioteki androidx.lifecycle, który jest powiązany z cyklem życia ViewModel i automatycznie anulowany przy jego czyszczeniu. Według Google Android Developers, 2025, viewModelScope jest standardowym mechanizmem uruchamiania korutyn w architekturze MVVM, zapewniając bezpieczną pracę z operacjami asynchronicznymi bez ryzyka wycieku pamięci. ViewModelScope domyślnie używa Dispatchers.Main, a wszystkie operacje IO wewnątrz niego muszą być wykonywane przez withContext.
Najważniejsze
viewModelScope — to właściwość rozszerzająca (extension property) na interfejs ViewModel, dodana w bibliotece lifecycle-viewmodel-ktx (od wersji 2.1.0). Zapewnia gotowy CoroutineScope, powiązany z cyklem życia ViewModel.
// Wewnętrzna struktura (uproszczona)
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope = this.getTag(JOB_KEY)
if (scope != null) return scope
return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { setTag(JOB_KEY, it) }
}
Scope jest tworzony leniwie (lazy) przy pierwszym dostępie i cachowany przez setTag. Używany jest SupervisorJob, co oznacza, że wyjątek w jednej korutynie potomnej nie anuluje pozostałych. Domyślnym dyspozytorem jest Dispatchers.Main.immediate, który wykonuje kod na głównym wątku bez dodatkowej dyspozycji, jeśli wywołanie jest już na Main.
Gdy ViewModel opuszcza cykl życia (Activity zakończone lub Fragment usunięty), system wywołuje clear(), który uruchamia onCleared(). W tym callbacku viewModelScope anuluje swój Job, co rekurencyjnie kończy wszystkie aktywne korutyny. Mechanizm jest zaimplementowany przez interfejs Closeable, gdzie Job scope jest rejestrowany jako zasób do automatycznego zamknięcia.
Mechanizm powiązania viewModelScope z cyklem życia ViewModel opiera się na tagowaniu i callbacku onCleared. Rozważmy krok po kroku, jak to działa.
Gdy ViewModel wykonuje viewModelScope.launch { ... }, getter sprawdza, czy istnieje zapisany scope pod tagiem JOB_KEY. Jeśli scope jeszcze nie istnieje — tworzona jest nowa instancja CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Scope jest przechowywany wewnątrz ViewModel przez wewnętrzną mapę tagów.
Wszystkie korutyny uruchomione przez viewModelScope.launch lub viewModelScope.async stają się potomne względem SupervisorJob scope. Działają na głównym wątku (jeśli nie określono innego dyspozytora przez withContext). Dopóki ViewModel żyje — korutyny mogą być aktywne, zawieszone lub zakończone.
Gdy system niszczy ViewModel, wywoływane jest ViewModel.clear(). Wewnątrz clear() dzieje się następująco:
Przy obrocie ekranu Activity jest odtwarzane, ale ViewModel jest zachowywany (dzięki ViewModelStoreOwner). Oznacza to, że viewModelScope pozostaje aktywny, a korutyny mogą kontynuować działanie bez przerw. Po odtworzeniu Activity ten sam ViewModel (i ten sam scope) jest używany ponownie — ładowanie danych nie zaczyna się od nowa.
MVVM (Model-View-ViewModel) — to zalecana przez Google architektura dla aplikacji Android. viewModelScope zajmuje w niej centralne miejsce jako wykonawca operacji asynchronicznych.
| Warstwa | Komponent | Rola viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Obserwuje StateFlow/LiveData z ViewModel |
| ViewModel | ViewModel | Uruchamia korutyny przez viewModelScope, zarządza stanem UI |
| Repository | Repository | Przyjmuje suspend-funkcje wywoływane z korutyn viewModelScope |
| Data | DAO / Api | Wykonuje rzeczywiste zapytania (Room, Retrofit) |
ViewModel przez viewModelScope uruchamia korutyny, wewnątrz których wywołuje suspend-funkcje Repository. Wynik jest przekształcany w StateFlow, który jest obserwowany przez warstwę UI. Taki schemat zapewnia wyraźne rozdzielenie odpowiedzialności i testowalność każdej warstwy niezależnie.
Gdyby korutyny były uruchamiane z Fragmentu, przy obrocie ekranu zostałyby anulowane wraz ze zniszczeniem Fragmentu. ViewModel przetrwa rotację, więc korutyny uruchomione w jego scope kontynuują działanie. To kluczowa zaleta viewModelScope nad lifecycleScope przy ładowaniu danych.
Rozważmy trzy praktyczne scenariusze użycia viewModelScope w aplikacji Android w Kotlin.
class ProfileViewModel(
private val repo: ProfileRepository
) : ViewModel() {
private val _profile = MutableStateFlow<Profile?>(null)
val profile: StateFlow<Profile?> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val result = repo.getProfile()
_profile.value = result
}
}
}
W bloku init od razu uruchamiane jest ładowanie profilu. Korutyna wykonuje się na głównym wątku (domyślnie). Repozytorium używa withContext(Dispatchers.IO) do zapytania sieciowego wewnątrz swojej suspend-funkcji, więc ViewModel nie musi zajmować się przełączaniem wątków.
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
fun fetchItems() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val items = repo.getItems()
_state.value = UiState.Success(items)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
Stan UI jest opisany przez sealed class UiState. ViewModel aktualizuje state przy każdej zmianie. Fragment jest subskrybowany na StateFlow i reaguje tylko na aktualny stan, ignorując nieaktualne wywołania przy kolejnych rotacjach.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
Przy każdym nowym zapytaniu wyszukiwania poprzednia korutyna jest anulowana. delay(300) implementuje debounce — wyszukiwanie jest wykonywane dopiero po 300 ms pauzy w wprowadzaniu. Zmniejsza to obciążenie serwera i zapobiega nieaktualnym wynikom.
Oba scope są dostarczane przez bibliotekę AndroidX Lifecycle, ale są powiązane z różnymi cyklami życia. Wybór między nimi zależy od typu zadania.
| Cecha | viewModelScope | lifecycleScope |
|---|---|---|
| Właściciel | ViewModel | LifecycleOwner (Activity/Fragment) |
| Anulowanie przy rotacji | Nie (ViewModel jest zachowywany) | Tak (Activity jest odtwarzane) |
| Domyślny dyspozytor | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Dostępny w | ViewModel | Activity, Fragment, Service |
| Typowy scenariusz | Ładowanie danych, logika biznesowa | Interakcja UI, animacje, snackbary |
Google zaleca używanie viewModelScope do wszystkich zadań związanych z ładowaniem i przetwarzaniem danych. lifecycleScope należy stosować do operacji powiązanych z konkretnym momentem życia UI — na przykład uruchamianie animacji przy pierwszym pojawieniu się ekranu lub subskrypcja aktualizacji Location, która powinna zakończyć się przy opuszczeniu ekranu.
Nawet w dobrze udokumentowanym API Android programiści popełniają charakterystyczne błędy. Rozważmy cztery najczęstsze problemy.
Najbardziej podstępny błąd — próba aktualizacji StateFlow lub LiveData po tym, jak ViewModel został wyczyszczony. Mimo że viewModelScope jest anulowany przy onCleared(), korutyna może wykonać kod do momentu faktycznego anulowania. Używaj isActive do sprawdzenia lub polegaj na zakończeniu bloku catch.
viewModelScope wewnętrznie używa SupervisorJob, co izoluje błędy między korutynami. Ale jeśli uruchomisz korutynę z własnym Job() wewnątrz viewModelScope.launch, ta korutyna stanie się potomna wobec SupervisorJob, ale nie będzie chroniona przed anulowaniem przy błędach w innych korutynach.
Mimo że viewModelScope nie ma sztywnego limitu, tysiące aktywnych korutyn mogą spowolnić system. Dla długich list danych używaj Flow z collectLatest zamiast tworzenia osobnych korutyn dla każdego elementu.
Jeśli przypadkowo zaimportujesz GlobalScope zamiast viewModelScope, korutyna nie zostanie anulowana przy czyszczeniu ViewModel. Doprowadzi to do wycieku pamięci i potencjalnego crasha. Zawsze sprawdzaj, że korutyny są uruchamiane przez viewModelScope, szczególnie w fragmentach dziedziczących.
Często zadawane pytania
Bezpośrednio zmienić dyspozytora viewModelScope nie można — jest sztywno ustawiony jako Dispatchers.Main.immediate. Ale wewnątrz korutyny można przełączyć się na inny dyspozytor przez withContext. Do zmiany dyspozytora w testach używaj TestDispatcher przez Rule.
Nie przekazuj scope do Repository — narusza to zasady architektury. Repository powinno dostarczać suspend-funkcje, a ViewModel sama zarządza korutynami przez viewModelScope. Jeśli Repository wymaga scope — przejrzyj architekturę na korzyść Clean Architecture.
SupervisorJob gwarantuje, że wyjątek w jednej korutynie (na przykład błąd ładowania jednego z kilku niezależnych żądań) nie anuluje pozostałych korutyn. To odpowiada scenariuszowi ViewModel, gdzie różne ekrany ładują niezależne dane.
Tak, viewModelScope jest dostępny w każdej ViewModel niezależnie od typu UI (View System lub Jetpack Compose). W Compose korutyny również są uruchamiane przez viewModelScope, a do efektów UI używa się LaunchedEffect i rememberCoroutineScope.
Wywołanie viewModelScope.cancel() anuluje scope natychmiast — wszystkie aktywne korutyny kończą się z CancellationException. Jeśli po tym wywołasz viewModelScope.launch, nowy scope zostanie utworzony automatycznie przy następnym dostępie do gettera.
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ż