viewModelScope: co to jest, powiązanie z ViewModel i działanie w Androidzie

Autor: IT Sectr Opublikowano: 2026-06-23 Czas czytania: 9 min

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 — CoroutineScope z lifecycle-viewmodel-ktx, anulowany przy onCleared() ViewModel
  • Dispatchers.Main — domyślny dyspozytor, więc aktualizacje UI wewnątrz korutyn są bezpieczne
  • onCleared — callback, przy wywołaniu którego viewModelScope automatycznie anuluje wszystkie aktywne korutyny
  • clear() vs onCleared() — clear() jest wywoływany przez framework przed onCleared, co gwarantuje anulowanie scope
  • launch — podstawowy sposób uruchamiania korutyn w viewModelScope dla operacji fire-and-forget

Czym jest viewModelScope w Androidzie?

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.

kotlin
// 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.

Jak viewModelScope otrzymuje powiadomienie o czyszczeniu

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.

Jak działa viewModelScope: powiązanie z cyklem życia ViewModel

Mechanizm powiązania viewModelScope z cyklem życia ViewModel opiera się na tagowaniu i callbacku onCleared. Rozważmy krok po kroku, jak to działa.

Krok 1: Utworzenie scope przy pierwszym dostępie

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.

Krok 2: Cykl życia korutyn

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.

Krok 3: Anulowanie przy onCleared

Gdy system niszczy ViewModel, wywoływane jest ViewModel.clear(). Wewnątrz clear() dzieje się następująco:

  • Wywołanie onCleared() dla logiki użytkownika
  • Zamknięcie wszystkich zasobów Closeable zarejestrowanych przez addCloseable
  • Job viewModelScope przechodzi w stan Cancelled
  • Wszystkie korutyny potomne są rekurencyjnie anulowane
  • Zwolnienie referencji do scope dla garbage collectora

Odporność na rotację ekranu

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.

viewModelScope w architekturze MVVM

MVVM (Model-View-ViewModel) — to zalecana przez Google architektura dla aplikacji Android. viewModelScope zajmuje w niej centralne miejsce jako wykonawca operacji asynchronicznych.

Rola viewModelScope w warstwach architektury

WarstwaKomponentRola viewModelScope
UIActivity / FragmentObserwuje StateFlow/LiveData z ViewModel
ViewModelViewModelUruchamia korutyny przez viewModelScope, zarządza stanem UI
RepositoryRepositoryPrzyjmuje suspend-funkcje wywoływane z korutyn viewModelScope
DataDAO / ApiWykonuje 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.

Dlaczego viewModelScope w ViewModel, a nie w Fragment

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.

Przykłady użycia viewModelScope

Rozważmy trzy praktyczne scenariusze użycia viewModelScope w aplikacji Android w Kotlin.

Przykład 1: Ładowanie danych przy tworzeniu ViewModel

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.

Przykład 2: Obsługa błędów przez sealed class

kotlin
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<Item>) : UiState()
    data class Error(val message: String) : UiState()
}
kotlin
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.

Przykład 3: Anulowanie poprzedniej korutyny przy nowym żądaniu

kotlin
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.

viewModelScope vs lifecycleScope: kiedy co wybrać

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.

Porównanie scope

CechaviewModelScopelifecycleScope
WłaścicielViewModelLifecycleOwner (Activity/Fragment)
Anulowanie przy rotacjiNie (ViewModel jest zachowywany)Tak (Activity jest odtwarzane)
Domyślny dyspozytorDispatchers.Main.immediateDispatchers.Main.immediate
Dostępny wViewModelActivity, Fragment, Service
Typowy scenariuszŁadowanie danych, logika biznesowaInterakcja UI, animacje, snackbary

Zalecenia Google

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.

Typowe błędy przy pracy z viewModelScope

Nawet w dobrze udokumentowanym API Android programiści popełniają charakterystyczne błędy. Rozważmy cztery najczęstsze problemy.

Błąd 1: Aktualizacja UI po anulowaniu scope

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.

Błąd 2: Uruchamianie korutyn bez uwzględnienia SupervisorJob

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.

Błąd 3: Zbyt wiele korutyn w jednym scope

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.

Błąd 4: Używanie GlobalScope zamiast viewModelScope

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

Czy można zmienić domyślny dyspozytor viewModelScope?

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.

Jak przekazać viewModelScope do Repository?

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.

Dlaczego viewModelScope używa SupervisorJob?

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.

Czy viewModelScope jest dostępny w Jetpack Compose?

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.

Co się stanie z korutyną przy wywołaniu viewModelScope.cancel()?

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

  • viewModelScope — CoroutineScope powiązany z cyklem życia ViewModel i automatycznie anulowany przy onCleared()
  • SupervisorJob + Dispatchers.Main — wewnętrzna konfiguracja zapewniająca izolację błędów i bezpieczny dostęp do UI
  • Rotacja ekranu — ViewModel jest zachowywany, więc korutyny w viewModelScope kontynuują działanie bez restartu
  • Architektura MVVM — viewModelScope jest centralnym elementem dla operacji asynchronicznych w warstwie ViewModel
  • lifecycleScope — alternatywa dla operacji powiązanych z cyklem życia Activity/Fragment, a nie ViewModel
  • StateFlow — preferowany sposób przesyłania danych z korutyn viewModelScope do UI przez sealed class
  • GlobalScope jest niebezpieczny — zastąpienie viewModelScope na GlobalScope prowadzi do wycieków pamięci i awarii aplikacji

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ż