viewModelScope: cos'è, collegamento a ViewModel e funzionamento in Android

Autore: IT Sectr Pubblicato: 2026-06-23 Tempo di lettura: 9 min

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 — CoroutineScope da lifecycle-viewmodel-ktx, annullato quando viene chiamato ViewModel.onCleared()
  • Dispatchers.Main — dispatcher predefinito, quindi gli aggiornamenti dell'UI all'interno delle coroutine sono sicuri
  • onCleared — callback che attiva l'annullamento automatico di tutte le coroutine attive in viewModelScope
  • clear() vs onCleared() — clear() viene chiamato dal framework prima di onCleared, garantendo l'annullamento dello scope
  • launch — il modo principale per avviare coroutine in viewModelScope per operazioni fire-and-forget

Cos'è viewModelScope in Android?

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.

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

Come viewModelScope riceve la notifica di pulizia

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.

Come funziona viewModelScope: collegamento al ciclo di vita di ViewModel

Il meccanismo di collegamento di viewModelScope al ciclo di vita di ViewModel si basa sul tagging e sul callback onCleared. Vediamolo passo dopo passo.

Passo 1: Creazione dello scope al primo accesso

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.

Passo 2: Ciclo di vita delle coroutine

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.

Passo 3: Annullamento in onCleared

Quando il sistema distrugge il ViewModel, viene chiamato ViewModel.clear(). All'interno di clear(), accade quanto segue:

  • onCleared() viene chiamato per la logica di pulizia personalizzata
  • Tutte le risorse Closeable registrate tramite addCloseable vengono chiuse
  • Il Job di viewModelScope passa allo stato Cancelled
  • Tutte le coroutine figlie vengono annullate ricorsivamente
  • I riferimenti allo scope vengono rilasciati per il garbage collector

Resistenza alla rotazione

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.

viewModelScope nell'architettura MVVM

MVVM (Model-View-ViewModel) è l'architettura consigliata da Google per le applicazioni Android. viewModelScope occupa un posto centrale come esecutore di operazioni asincrone.

Il ruolo di viewModelScope nei livelli architetturali

LivelloComponenteRuolo di viewModelScope
UIActivity / FragmentOsserva StateFlow/LiveData dal ViewModel
ViewModelViewModelLancia coroutine tramite viewModelScope, gestisce lo stato dell'UI
RepositoryRepositoryEspone funzioni suspend chiamate dalle coroutine di viewModelScope
DataDAO / ApiEsegue 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.

Perché viewModelScope nel ViewModel e non nel Fragment

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.

Esempi di utilizzo di viewModelScope

Esaminiamo tre scenari pratici di utilizzo di viewModelScope in un'applicazione Android con Kotlin.

Esempio 1: Caricamento dati alla creazione del 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
        }
    }
}

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.

Esempio 2: Gestione degli errori con 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")
        }
    }
}

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.

Esempio 3: Annullamento della coroutine precedente su nuova richiesta

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
    }
}

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.

viewModelScope vs lifecycleScope: quando scegliere l'uno o l'altro

Entrambi gli scope sono forniti dalla libreria AndroidX Lifecycle, ma sono legati a diversi cicli di vita. La scelta dipende dal tipo di attività.

Confronto tra scope

CaratteristicaviewModelScopelifecycleScope
ProprietarioViewModelLifecycleOwner (Activity/Fragment)
Annullato in rotazioneNo (ViewModel sopravvive)Sì (Activity viene ricreata)
Dispatcher predefinitoDispatchers.Main.immediateDispatchers.Main.immediate
Disponibile inViewModelActivity, Fragment, Service
Caso d'uso tipicoCaricamento dati, logica di businessInterazioni UI, animazioni

Raccomandazioni Google

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.

Errori comuni quando si lavora con viewModelScope

Anche in un'API Android ben documentata, gli sviluppatori commettono errori tipici. Esaminiamo quattro dei problemi più comuni.

Errore 1: Aggiornare l'UI dopo l'annullamento dello scope

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.

Errore 2: Lanciare coroutine senza considerare SupervisorJob

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.

Errore 3: Troppe coroutine in un unico scope

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.

Errore 4: Utilizzare GlobalScope invece di viewModelScope

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

Posso cambiare il dispatcher predefinito di viewModelScope?

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.

Come passo viewModelScope al Repository?

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.

Perché viewModelScope usa SupervisorJob?

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.

viewModelScope è disponibile in Jetpack Compose?

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.

Cosa succede a una coroutine quando viene chiamato viewModelScope.cancel()?

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

  • viewModelScope — CoroutineScope legato al ciclo di vita di ViewModel, automaticamente annullato in onCleared()
  • SupervisorJob + Dispatchers.Main — configurazione interna che garantisce isolamento degli errori e accesso sicuro all'UI
  • Rotazione schermo — il ViewModel sopravvive, quindi le coroutine in viewModelScope continuano senza riavvio
  • Architettura MVVM — viewModelScope è l'elemento centrale per le operazioni asincrone nel livello ViewModel
  • lifecycleScope — alternativa per operazioni legate al ciclo di vita di Activity/Fragment, non a ViewModel
  • StateFlow — modo preferito per passare dati dalle coroutine di viewModelScope all'UI tramite sealed class
  • GlobalScope è pericoloso — sostituire viewModelScope con GlobalScope porta a perdite di memoria e crash dell'app

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.

Discuti il progetto

Leggi anche