viewModelScope è un CoroutineScope integrato della libreria androidx.lifecycle che è legato al ciclo di vita di ViewModel e viene automaticamente annullato quando viene pulito. Secondo Google Android Developers, 2025, viewModelScope è il meccanismo standard per lanciare coroutine nell'architettura MVVM, garantendo operazioni asincrone sicure senza rischio di perdite di memoria. ViewModelScope utilizza Dispatchers.Main per impostazione predefinita e tutte le operazioni IO al suo interno devono essere eseguite tramite withContext.
Punti chiave
viewModelScope è una proprietà di estensione sull'interfaccia ViewModel, aggiunta nella libreria lifecycle-viewmodel-ktx (a partire dalla versione 2.1.0). Fornisce un CoroutineScope pronto all'uso legato al ciclo di vita di ViewModel.
// Internal structure (simplified)
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) }
}
Lo scope viene creato in modo lazy al primo accesso e memorizzato nella cache tramite setTag. Utilizza SupervisorJob, il che significa che un'eccezione in una coroutine figlia non annulla le altre. Il dispatcher predefinito è Dispatchers.Main.immediate, che esegue il codice sul thread principale senza ulteriore dispatch se già su Main.
Quando il ViewModel lascia il ciclo di vita (l'Activity viene terminata o il Fragment viene rimosso), il sistema chiama clear(), che attiva onCleared(). In questo callback, viewModelScope annulla il suo Job, che termina ricorsivamente tutte le coroutine attive. Il meccanismo è implementato tramite l'interfaccia Closeable, dove il Job dello scope viene registrato come risorsa per la chiusura automatica.
Il meccanismo di collegamento di viewModelScope al ciclo di vita di ViewModel si basa sul tagging e sul callback onCleared. Vediamolo passo dopo passo.
Quando il ViewModel esegue viewModelScope.launch { ... }, il getter verifica se esiste già uno scope memorizzato sotto il tag JOB_KEY. Se non esiste uno scope, viene creata una nuova istanza di CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate). Lo scope viene memorizzato all'interno del ViewModel tramite una mappa interna di tag.
Tutte le coroutine lanciate tramite viewModelScope.launch o viewModelScope.async diventano figlie del SupervisorJob dello scope. Eseguono sul thread principale (a meno che non venga specificato un dispatcher diverso tramite withContext). Finchè il ViewModel è vivo, le coroutine possono essere attive, sospese o completate.
Quando il sistema distrugge il ViewModel, viene chiamato ViewModel.clear(). All'interno di clear(), accade quanto segue:
Quando lo schermo viene ruotato, l'Activity viene ricreata, ma il ViewModel sopravvive (grazie a ViewModelStoreOwner). Ciò significa che viewModelScope rimane attivo e le coroutine continuano l'esecuzione senza interruzioni. Dopo la ricreazione dell'Activity, lo stesso ViewModel (e lo stesso scope) viene riutilizzato — il caricamento dei dati non ricomincia da capo.
MVVM (Model-View-ViewModel) è l'architettura consigliata da Google per le applicazioni Android. viewModelScope occupa un posto centrale come esecutore di operazioni asincrone.
| Livello | Componente | Ruolo di viewModelScope |
|---|---|---|
| UI | Activity / Fragment | Osserva StateFlow/LiveData dal ViewModel |
| ViewModel | ViewModel | Lancia coroutine tramite viewModelScope, gestisce lo stato dell'UI |
| Repository | Repository | Espone funzioni suspend chiamate dalle coroutine di viewModelScope |
| Data | DAO / Api | Esegue le richieste effettive (Room, Retrofit) |
Il ViewModel lancia coroutine tramite viewModelScope, all'interno delle quali chiama funzioni suspend del Repository. Il risultato viene trasformato in StateFlow, che viene osservato dal livello UI. Questo design garantisce una chiara separazione delle responsabilità e la testabilità indipendente di ogni livello.
Se le coroutine venissero lanciate da un Fragment, verrebbero annullate durante la rotazione dello schermo insieme alla distruzione del Fragment. Il ViewModel sopravvive alla rotazione, quindi le coroutine lanciate nel suo scope continuano l'esecuzione. Questo è il vantaggio chiave di viewModelScope rispetto a lifecycleScope durante il caricamento dei dati.
Esaminiamo tre scenari pratici di utilizzo di viewModelScope in un'applicazione Android con 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
}
}
}
Nel blocco init, il caricamento del profilo inizia immediatamente. La coroutine viene eseguita sul thread principale (per impostazione predefinita). Il repository utilizza withContext(Dispatchers.IO) per la richiesta di rete all'interno della sua funzione suspend, quindi il ViewModel non deve preoccuparsi del cambio di thread.
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")
}
}
}
Lo stato dell'UI viene descritto tramite una sealed class UiState. Il ViewModel aggiorna lo stato a ogni modifica. Il Fragment si iscrive a StateFlow e reagisce solo allo stato corrente, ignorando le chiamate obsolete da rotazioni precedenti.
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
A ogni nuova query di ricerca, la coroutine precedente viene annullata. delay(300) implementa il debounce — la ricerca viene eseguita solo dopo 300 ms di inattività. Questo riduce il carico del server e previene risultati obsoleti.
Entrambi gli scope sono forniti dalla libreria AndroidX Lifecycle, ma sono legati a diversi cicli di vita. La scelta dipende dal tipo di attività.
| Caratteristica | viewModelScope | lifecycleScope |
|---|---|---|
| Proprietario | ViewModel | LifecycleOwner (Activity/Fragment) |
| Annullato in rotazione | No (ViewModel sopravvive) | Sì (Activity viene ricreata) |
| Dispatcher predefinito | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| Disponibile in | ViewModel | Activity, Fragment, Service |
| Caso d'uso tipico | Caricamento dati, logica di business | Interazioni UI, animazioni |
Google raccomanda di utilizzare viewModelScope per tutte le attività di caricamento e elaborazione dati. lifecycleScope dovrebbe essere utilizzato per operazioni legate a un momento specifico del ciclo di vita dell'UI — ad esempio, avviare un'animazione alla prima apparizione dello schermo o iscriversi agli aggiornamenti di posizione che devono fermarsi quando si lascia lo schermo.
Anche in un'API Android ben documentata, gli sviluppatori commettono errori tipici. Esaminiamo quattro dei problemi più comuni.
L'errore più insidioso è tentare di aggiornare StateFlow o LiveData dopo che il ViewModel è stato pulito. Sebbene viewModelScope venga annullato in onCleared(), una coroutine potrebbe eseguire codice prima che l'annullamento abbia effetto. Utilizza isActive per verificare o affidati al completamento del blocco catch.
viewModelScope utilizza internamente SupervisorJob, che isola gli errori tra le coroutine. Tuttavia, se lanci una coroutine con il proprio Job() all'interno di viewModelScope.launch, quella coroutine diventa figlia di SupervisorJob ma non sarà protetta dall'annullamento causato da errori in altre coroutine.
Sebbene viewModelScope non abbia un limite rigido, migliaia di coroutine attive possono rallentare il sistema. Per lunghe liste di dati, utilizza Flow con collectLatest invece di creare coroutine separate per ogni elemento.
Se GlobalScope viene importato accidentalmente invece di viewModelScope, la coroutine non verrà annullata alla pulizia del ViewModel. Ciò porta a perdite di memoria e potenziali crash. Assicurati sempre che le coroutine siano lanciate tramite viewModelScope, specialmente nelle sottoclassi di Fragment.
Domande frequenti
Non puoi cambiare direttamente il dispatcher di viewModelScope — è hardcodato come Dispatchers.Main.immediate. Tuttavia, all'interno di una coroutine puoi passare a un altro dispatcher tramite withContext. Per cambiare il dispatcher nei test, utilizza TestDispatcher tramite una Rule.
Non passare lo scope al Repository — questo viola i principi architetturali. Il Repository dovrebbe esporre funzioni suspend, e il ViewModel stesso gestisce le coroutine tramite viewModelScope. Se il Repository richiede uno scope, riconsidera l'architettura a favore di Clean Architecture.
SupervisorJob garantisce che un'eccezione in una coroutine (ad esempio, un errore di caricamento in una di diverse richieste indipendenti) non annulli le altre coroutine. Questo corrisponde allo scenario ViewModel, dove schermate diverse caricano dati indipendenti.
Sì, viewModelScope è disponibile in qualsiasi ViewModel indipendentemente dal tipo di UI (View System o Jetpack Compose). In Compose, le coroutine vengono anch'esse lanciate tramite viewModelScope, mentre per gli effetti UI si utilizzano LaunchedEffect e rememberCoroutineScope.
Chiamare viewModelScope.cancel() annulla lo scope immediatamente — tutte le coroutine attive terminano con CancellationException. Se successivamente viene chiamato viewModelScope.launch, un nuovo scope viene creato automaticamente al prossimo accesso al getter.
Riepilogo
Svilupperemo un'applicazione mobile chiavi in mano
IT Sectr crea applicazioni iOS e Android per startup e aziende dal 2017. Ti consulteremo e ti proporremo la soluzione migliore.
Leggi anche