viewModelScope — este un CoroutineScope încorporat din biblioteca androidx.lifecycle, care este legat de ciclul de viață al ViewModel și se anulează automat la curățarea acestuia. Potrivit Google Android Developers, 2025, viewModelScope este mecanismul standard de lansare a corutinelor în arhitectura MVVM, asigurând lucrul sigur cu operații asincrone fără riscul de scurgeri de memorie. ViewModelScope folosește implicit Dispatchers.Main, iar toate operațiile IO din interior trebuie efectuate prin withContext.
Principalele puncte
viewModelScope — este o proprietate de extensie (extension property) pe interfața ViewModel, adăugată în biblioteca lifecycle-viewmodel-ktx (începând cu versiunea 2.1.0). Oferă un CoroutineScope gata făcut, legat de ciclul de viață al ViewModel.
// Structura internă (simplificată)
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-ul este creat leneș (lazy) la prima accesare și este stocat în cache prin setTag. Se folosește SupervisorJob, ceea ce înseamnă că o excepție într-o corutină copil nu le anulează pe celelalte. Dispatcherul implicit este Dispatchers.Main.immediate, care execută codul pe firul principal fără dispatchere suplimentară, dacă apelul este deja pe Main.
Când ViewModel părăsește ciclul de viață (Activity este finalizată sau Fragment este șters), sistemul apelează clear(), care declanșează onCleared(). În acest callback, viewModelScope își anulează Job-ul, ceea ce termină recursive toate corutinele active. Mecanismul este implementat prin interfața Closeable, unde Job-ul scope-ului este înregistrat ca resursă pentru închidere automată.
Mecanismul de legare a viewModelScope la ciclul de viață al ViewModel se bazează pe etichetare și callback-ul onCleared. Să analizăm pas cu pas cum funcționează.
Când ViewModel execută viewModelScope.launch { ... }, getter-ul verifică dacă există un scope salvat sub eticheta JOB_KEY. Dacă scope-ul nu a fost încă creat — se creează o nouă instanță CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Scope-ul este stocat în interiorul ViewModel printr-o hartă internă de etichete.
Toate corutinele lansate prin viewModelScope.launch sau viewModelScope.async devin copii față de SupervisorJob al scope-ului. Ele rulează pe firul principal (dacă nu este specificat un alt dispatcher prin withContext). Cât timp ViewModel este activ — corutinele pot fi active, suspendate sau finalizate.
Când sistemul distruge ViewModel, se apelează ViewModel.clear(). În interiorul clear() se întâmplă următoarele:
La rotirea ecranului, Activity este recreată, dar ViewModel este păstrat (datorită ViewModelStoreOwner). Aceasta înseamnă că viewModelScope rămâne activ, iar corutinele continuă să ruleze fără întrerupere. După recrearea Activity, același ViewModel (și același scope) este reutilizat — încărcarea datelor nu începe de la zero.
MVVM (Model-View-ViewModel) — este arhitectura recomandată de Google pentru aplicațiile Android. viewModelScope ocupă un loc central în ea ca executor al operațiilor asincrone.
| Strat | Componentă | Rolul viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Observă StateFlow/LiveData din ViewModel |
| ViewModel | ViewModel | Lansează corutine prin viewModelScope, gestionează starea UI |
| Repository | Repository | Primește funcții suspend apelate din corutinele viewModelScope |
| Date | DAO / Api | Execută cererile efective (Room, Retrofit) |
ViewModel prin viewModelScope lansează corutine, în interiorul cărora apelează funcțiile suspend ale Repository. Rezultatul este transformat în StateFlow, care este observat de stratul UI. Această schemă asigură o separare clară a responsabilităților și testabilitatea fiecărui strat în mod independent.
Dacă corutinele ar fi lansate din Fragment, la rotirea ecranului ele ar fi anulate odată cu distrugerea Fragmentului. ViewModel supraviețuiește rotației, deci corutinele lansate în scope-ul său continuă execuția. Acesta este avantajul cheie al viewModelScope față de lifecycleScope la încărcarea datelor.
Să analizăm trei scenarii practice de utilizare a viewModelScope într-o aplicație Android în 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
}
}
}
În blocul init se lansează imediat încărcarea profilului. Corutina rulează pe firul principal (implicit). Repository-ul folosește withContext(Dispatchers.IO) pentru cererea de rețea în interiorul funcției sale suspend, așadar ViewModel nu se ocupă de comutarea firelor.
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")
}
}
}
Starea UI este descrisă prin sealed class UiState. ViewModel actualizează state-ul la fiecare modificare. Fragment este abonat la StateFlow și reacționează doar la starea curentă, ignorând apelurile învechite la rotații repetate.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
La fiecare nouă cerere de căutare, corutina anterioară este anulată. delay(300) implementează debounce — căutarea se execută doar după 300 ms pauză în introducere. Aceasta reduce încărcarea serverului și previne rezultatele învechite.
Ambele scope-uri sunt furnizate de biblioteca AndroidX Lifecycle, dar sunt legate de cicluri de viață diferite. Alegerea între ele depinde de tipul sarcinii.
| Caracteristică | viewModelScope | lifecycleScope |
|---|---|---|
| Proprietar | ViewModel | LifecycleOwner (Activity/Fragment) |
| Anulare la rotație | Nu (ViewModel este păstrat) | Da (Activity este recreată) |
| Dispatcher implicit | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Disponibil în | ViewModel | Activity, Fragment, Service |
| Scenariu tipic | Încărcare date, logică de business | Interacțiune UI, animații, snackbar-uri |
Google recomandă utilizarea viewModelScope pentru toate sarcinile legate de încărcarea și procesarea datelor. lifecycleScope trebuie aplicat pentru operațiile legate de un moment specific al vieții UI — de exemplu, lansarea unei animații la prima apariție a ecranului sau abonarea la actualizări Location care trebuie să înceteze la părăsirea ecranului.
Chiar și în API-ul bine documentat al Android, dezvoltătorii fac greșeli caracteristice. Să analizăm patru probleme frecvente.
Cea mai perfidă eroare — încercarea de a actualiza StateFlow sau LiveData după ce ViewModel a fost curățat. Deși viewModelScope este anulat la onCleared(), corutina poate executa cod până la momentul anulării efective. Folosiți isActive pentru verificare sau bazați-vă pe finalizarea blocului catch.
viewModelScope folosește intern SupervisorJob, ceea ce izolează erorile între corutine. Dar dacă lansați o corutină cu propriul Job() în interiorul viewModelScope.launch, această corutină devine copil al SupervisorJob, dar nu va fi protejată de anulare la erori în alte corutine.
Deși viewModelScope nu are o limită strictă, mii de corutine active pot încetini sistemul. Pentru liste lungi de date, folosiți Flow cu collectLatest în loc să creați corutine separate pentru fiecare element.
Dacă importați din greșeală GlobalScope în loc de viewModelScope, corutina nu va fi anulată la curățarea ViewModel. Aceasta duce la scurgeri de memorie și potențiale crash-uri. Verificați întotdeauna că corutinele sunt lansate prin viewModelScope, în special în fragmentele moștenite.
Întrebări frecvente
Nu se poate schimba direct dispatcherul viewModelScope — este setat rigid ca Dispatchers.Main.immediate. Dar în interiorul corutinei se poate comuta pe un alt dispatcher prin withContext. Pentru schimbarea dispatcherului în teste, folosiți TestDispatcher prin Rule.
Nu transmiteți scope-ul în Repository — aceasta încalcă principiile arhitecturii. Repository trebuie să furnizeze funcții suspend, iar ViewModel gestionează singur corutinele prin viewModelScope. Dacă Repository necesită scope — revizuiți arhitectura în favoarea Clean Architecture.
SupervisorJob garantează că o excepție într-o corutină (de exemplu, eroarea de încărcare a uneia dintre mai multe cereri independente) nu anulează celelalte corutine. Acest lucru corespunde scenariului ViewModel, unde ecrane diferite încarcă date independente.
Da, viewModelScope este disponibil în orice ViewModel, indiferent de tipul UI (View System sau Jetpack Compose). În Compose, corutinele sunt de asemenea lansate prin viewModelScope, iar pentru efectele UI se folosesc LaunchedEffect și rememberCoroutineScope.
Apelarea viewModelScope.cancel() anulează scope-ul imediat — toate corutinele active se termină cu CancellationException. Dacă după aceasta apelați viewModelScope.launch, un nou scope este creat automat la următoarea accesare a getter-ului.
Concluzii
Vom dezvolta o aplicație mobilă la cheie
IT Sectr creează aplicații iOS și Android pentru startup-uri și afaceri din 2017. Vă vom consilia și vă vom propune cea mai bună soluție.
Citiți și