viewModelScope — je vestavěný CoroutineScope z knihovny androidx.lifecycle, který je svázán s životním cyklem ViewModel a automaticky se ruší při jeho čištění. Podle Google Android Developers, 2025 je viewModelScope standardním mechanismem spouštění korutin v architektuře MVVM, který poskytuje bezpečnou práci s asynchronními operacemi bez rizika úniku paměti. ViewModelScope standardně používá Dispatchers.Main a všechny IO operace uvnitř něj musí být prováděny přes withContext.
Hlavní body
viewModelScope — je rozšiřující vlastnost (extension property) na rozhraní ViewModel, přidaná v knihovně lifecycle-viewmodel-ktx (od verze 2.1.0). Poskytuje hotový CoroutineScope svázaný s životním cyklem ViewModel.
// Vnitřní struktura (zjednodušená)
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 je vytvářen líně (lazy) při prvním přístupu a je ukládán do mezipaměti přes setTag. Používá se SupervisorJob, což znamená, že výjimka v jedné podřízené korutině neruší ostatní. Výchozí dispatcher je Dispatchers.Main.immediate, který spouští kód na hlavním vlákně bez dalšího dispatchování, pokud je volání již na Main.
Když ViewModel opustí životní cyklus (Activity je dokončena nebo Fragment je odstraněn), systém zavolá clear(), který spustí onCleared(). V tomto callbacku viewModelScope ruší svůj Job, což rekurzivně ukončí všechny aktivní korutiny. Mechanismus je implementován přes rozhraní Closeable, kde je Job scope registrován jako prostředek pro automatické uzavření.
Mechanismus vazby viewModelScope na životní cyklus ViewModel je založen na tagování a callbacku onCleared. Podívejme se krok za krokem, jak to funguje.
Když ViewModel provádí viewModelScope.launch { ... }, getter zkontroluje, zda existuje uložený scope pod tagem JOB_KEY. Pokud scope ještě není vytvořen — vytvoří se nová instance CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Scope je uložen uvnitř ViewModel přes vnitřní mapu tagů.
Všechny korutiny spuštěné přes viewModelScope.launch nebo viewModelScope.async se stávají podřízenými vůči SupervisorJob scope. Pracují na hlavním vlákně (pokud není uveden jiný dispatcher přes withContext). Dokud je ViewModel živý — korutiny mohou být aktivní, pozastavené nebo dokončené.
Když systém ničí ViewModel, je voláno ViewModel.clear(). Uvnitř clear() dochází k následujícímu:
Při rotaci obrazovky je Activity znovu vytvořeno, ale ViewModel je zachován (díky ViewModelStoreOwner). To znamená, že viewModelScope zůstává aktivní a korutiny pokračují v běhu bez přerušení. Po znovuvytvoření Activity je stejný ViewModel (a stejný scope) použit znovu — načítání dat nezačíná od začátku.
MVVM (Model-View-ViewModel) — je architektura doporučená Googlem pro Android aplikace. viewModelScope v ní zaujímá ústřední místo jako vykonavatel asynchronních operací.
| Vrstva | Komponent | Role viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Sleduje StateFlow/LiveData z ViewModel |
| ViewModel | ViewModel | Spouští korutiny přes viewModelScope, spravuje stav UI |
| Repository | Repository | Přijímá suspend-funkce volané z korutin viewModelScope |
| Data | DAO / Api | Provádí skutečné dotazy (Room, Retrofit) |
ViewModel přes viewModelScope spouští korutiny, uvnitř kterých volá suspend-funkce Repository. Výsledek je převeden na StateFlow, který je sledován UI vrstvou. Toto schéma zajišťuje jasné oddělení odpovědností a testovatelnost každé vrstvy nezávisle.
Pokud by korutiny byly spouštěny z Fragmentu, při rotaci obrazovky by byly zrušeny spolu se zničením Fragmentu. ViewModel přežije rotaci, takže korutiny spuštěné v jeho scope pokračují v běhu. To je klíčová výhoda viewModelScope oproti lifecycleScope při načítání dat.
Podívejme se na tři praktické scénáře použití viewModelScope v Android aplikaci v 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
}
}
}
V bloku init je okamžitě spuštěno načítání profilu. Korutina běží na hlavním vlákně (standardně). Repository používá withContext(Dispatchers.IO) pro síťový požadavek uvnitř své suspend-funkce, takže ViewModel se nemusí starat o přepínání vláken.
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")
}
}
}
Stav UI je popsán přes sealed class UiState. ViewModel aktualizuje state při každé změně. Fragment je přihlášen k odběru StateFlow a reaguje pouze na aktuální stav, ignoruje zastaralá volání při opakovaných rotacích.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
Při každém novém vyhledávacím požadavku je předchozí korutina zrušena. delay(300) implementuje debounce — vyhledávání se provede až po 300 ms pauze ve vstupu. To snižuje zatížení serveru a zabraňuje zastaralým výsledkům.
Oba scope jsou poskytovány knihovnou AndroidX Lifecycle, ale jsou svázány s různými životními cykly. Volba mezi nimi závisí na typu úkolu.
| Vlastnost | viewModelScope | lifecycleScope |
|---|---|---|
| Vlastník | ViewModel | LifecycleOwner (Activity/Fragment) |
| Zrušení při rotaci | Ne (ViewModel je zachován) | Ano (Activity je znovu vytvořeno) |
| Výchozí dispatcher | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Dostupný v | ViewModel | Activity, Fragment, Service |
| Typický scénář | Načítání dat, obchodní logika | Interakce UI, animace, snackbary |
Google doporučuje používat viewModelScope pro všechny úkoly související s načítáním a zpracováním dat. lifecycleScope by měl být aplikován pro operace vázané na konkrétní okamžik života UI — například spuštění animace při prvním zobrazení obrazovky nebo přihlášení k odběru Location aktualizací, které by měly skončit při opuštění obrazovky.
I v dobře zdokumentovaném API Androidu dělají vývojáři charakteristické chyby. Podívejme se na čtyři nejčastější problémy.
Nejzákeřnější chyba — pokus o aktualizaci StateFlow nebo LiveData po vyčištění ViewModel. Přestože je viewModelScope zrušen při onCleared(), korutina může provést kód až do okamžiku skutečného zrušení. Použijte isActive pro kontrolu nebo se spolehněte na dokončení catch bloku.
viewModelScope interně používá SupervisorJob, což izoluje chyby mezi korutinami. Ale pokud spustíte korutinu s vlastním Job() uvnitř viewModelScope.launch, tato korutina se stane podřízenou SupervisorJob, ale nebude chráněna před zrušením při chybách v jiných korutinách.
Ačkoli viewModelScope nemá přísný limit, tisíce aktivních korutin mohou zpomalit systém. Pro dlouhé seznamy dat použijte Flow s collectLatest místo vytváření samostatných korutin pro každý prvek.
Pokud omylem importujete GlobalScope místo viewModelScope, korutina nebude zrušena při čištění ViewModel. To vede k úniku paměti a potenciálnímu pádu. Vždy kontrolujte, že korutiny jsou spouštěny přes viewModelScope, zejména v zděděných fragmentech.
Často kladené otázky
Dispatcher viewModelScope nelze přímo změnit — je pevně nastaven jako Dispatchers.Main.immediate. Ale uvnitř korutiny lze přes withContext přepnout na jiný dispatcher. Pro změnu dispatcheru v testech použijte TestDispatcher přes Rule.
Nepředávejte scope do Repository — to porušuje architektonické principy. Repository by mělo poskytovat suspend-funkce a ViewModel si samo spravuje korutiny přes viewModelScope. Pokud Repository vyžaduje scope — přehodnoťte architekturu ve prospěch Clean Architecture.
SupervisorJob zaručuje, že výjimka v jedné korutině (například chyba načítání jednoho z několika nezávislých požadavků) neruší ostatní korutiny. To odpovídá scénáři ViewModel, kde různé obrazovky načítají nezávislá data.
Ano, viewModelScope je dostupný v jakémkoli ViewModel, bez ohledu na typ UI (View System nebo Jetpack Compose). V Composable jsou korutiny také spouštěny přes viewModelScope a pro UI efekty se používají LaunchedEffect a rememberCoroutineScope.
Volání viewModelScope.cancel() ruší scope okamžitě — všechny aktivní korutiny končí s CancellationException. Pokud poté zavoláte viewModelScope.launch, nový scope je automaticky vytvořen při příštím přístupu ke getteru.
Shrnutí
Vyvineme mobilní aplikaci na klíč
IT Sectr vytváří aplikace pro iOS a Android pro startupy a podniky od roku 2017. Poradíme vám a navrhneme nejlepší řešení.
Přečtěte si také