CoroutineScope — cos'è, ciclo di vita e funzionamento nelle coroutine

Autore: IT Sectr Pubblicato: 2026-06-22 Tempo di lettura: 10 min

CoroutineScope è un'interfaccia Kotlin che definisce il ciclo di vita di una coroutine e fornisce un contesto per lanciare nuove coroutine. Secondo la documentazione Kotlin, 2025, ogni istanza di CoroutineScope contiene un CoroutineContext e gestisce tutte le coroutine lanciate al suo interno. Quando lo scope viene cancellato, tutte le coroutine figlie vengono automaticamente cancellate, prevenendo perdite di memoria.

Punti chiave

  • CoroutineScope — un'interfaccia con un singolo campo CoroutineContext, che definisce il ciclo di vita delle coroutine
  • Job — un elemento di contesto responsabile della cancellazione: cancellare lo scope cancella tutte le coroutine figlie
  • Concorrenza strutturata — il principio per cui le coroutine figlie sono legate allo scope padre
  • GlobalScope — uno scope per l'intera applicazione che non è raccomandato a causa del rischio di perdite di memoria
  • supervisorScope — uno scope speciale in cui la cancellazione di una coroutine figlia non cancella le altre

Cos'è CoroutineScope in Kotlin?

CoroutineScope è un'interfaccia fondamentale della libreria kotlinx.coroutines che funge da contenitore per le coroutine. Definisce i confini del ciclo di vita delle coroutine: quando lo scope viene completato, tutte le coroutine al suo interno vengono automaticamente cancellate.

kotlin
public interface CoroutineScope {
    public val coroutineContext: CoroutineContext
}

L'interfaccia contiene un solo campo — coroutineContext. Attraverso di esso, lo scope fornisce un dispatcher (Dispatcher), un job (Job), un gestore di eccezioni e altri elementi di contesto a tutte le coroutine lanciate al suo interno.

Ruolo nella libreria kotlinx.coroutines

Tutte le funzioni di lancio delle coroutine — launch, async, runBlocking — sono funzioni di estensione su CoroutineScope. Ciò significa che possono essere chiamate solo quando è disponibile un oggetto scope. Questo design garantisce che ogni coroutine abbia un padre e un ciclo di vita chiaramente definiti.

Dove viene utilizzato CoroutineScope

In Android, ogni componente architetturale ha il proprio scope: viewModelScope per ViewModel, lifecycleScope per Activity/Fragment. Nelle applicazioni server, uno scope può essere legato a una richiesta HTTP o a un pool di connessioni al database.

Come funziona CoroutineScope: Job e concorrenza strutturata

Comprendere il funzionamento interno di CoroutineScope richiede familiarità con il concetto di Job e il principio di concorrenza strutturata.

Job — il compito della coroutine

Ogni coroutine al lancio restituisce un oggetto Job (o Deferred per async). Job rappresenta un compito con un ciclo di vita finito: New, Active, Completing, Completed, Cancelling, Cancelled. Gli oggetti Job formano una struttura ad albero:

  • Job padre — lo scope in cui la coroutine è stata lanciata
  • Job figlio — ogni coroutine lanciata tramite launch/async
  • Cancellazione del padre → cancellazione di tutti i figli
  • Eccezione in un figlio → cancellazione del padre (eccetto in supervisorScope)

Il principio della concorrenza strutturata

Concorrenza strutturata è un principio architetturale chiave di Kotlin Coroutines, in cui il ciclo di vita della coroutine è legato al ciclo di vita del suo scope. Questo contrasta con il modello “spara e dimentica”, in cui una coroutine continua a vivere dopo il completamento dello scope. Vantaggi della concorrenza strutturata:

  • Ciclo di vita prevedibile — quando lo scope viene completato, tutte le coroutine vengono fermate
  • Gestione automatica degli errori — un'eccezione in qualsiasi coroutine figlia si propaga allo scope
  • Nessuna perdita di memoria — nessuna coroutine rimane in esecuzione dopo il completamento dello scope
  • Gerarchia chiara — il codice riflette la struttura logica delle operazioni parallele

Ciclo di vita di CoroutineScope

Quando viene chiamato scope.cancel(), il Job dello scope passa allo stato Cancelled, che cancella ricorsivamente tutti i Job figli. Dopo la cancellazione, lo scope può essere riutilizzato solo creando una nuova istanza di CoroutineScope.

Creazione e configurazione di CoroutineScope

Puoi creare un CoroutineScope tramite una funzione factory o implementando l'interfaccia nella tua classe. Esaminiamo entrambi gli approcci.

Funzione factory CoroutineScope()

kotlin
val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())

scope.launch {
    println("Running on ${Thread.currentThread().name}")
}

La funzione factory accetta un CoroutineContext e crea uno scope con il contesto specificato. L'esempio utilizza Dispatchers.Default per attività intensive di CPU e SupervisorJob, che isola le eccezioni tra le coroutine figlie.

Implementazione dell'interfaccia tramite composizione

kotlin
class MyRepository {
    private val scope = CoroutineScope(Dispatchers.IO + Job())

    suspend fun fetchData(): Data = scope.async {
        api.getData()
    }.await()

    fun cleanup() {
        scope.cancel()
    }
}

Memorizziamo lo scope come campo della classe e chiamiamo manualmente cleanup per cancellarlo. Questo approccio è adatto per componenti con ciclo di vita gestito — ad esempio, repository o manager.

Implementazione tramite delega

Kotlin consente di delegare l'implementazione di CoroutineScope tramite la parola chiave by:

kotlin
class DataLoader : CoroutineScope by CoroutineScope(Dispatchers.IO) {
    fun load() {
        launch {
            // coroutine runs in DataLoader scope
        }
    }
}

Questo approccio è conveniente quando la classe stessa è uno scope e vuole fornire metodi di lancio delle coroutine. Tuttavia, fai attenzione: la classe eredita tutti i metodi di CoroutineScope, incluso cancel, che può rompere l'incapsulamento.

GlobalScope vs CoroutineScope personalizzato

GlobalScope è un singleton CoroutineScope per l'intera applicazione. Il suo utilizzo in codice di produzione non è ufficialmente raccomandato.

Problemi con GlobalScope

  • Mancanza di concorrenza strutturata — le coroutine in GlobalScope non sono legate al ciclo di vita del componente
  • Perdite di memoria — una coroutine può continuare a essere eseguita dopo la chiusura dell'Activity/Fragment
  • Test difficili — GlobalScope non può essere sostituito nei test
  • Consumo non controllato di risorse — molte coroutine possono essere eseguite più a lungo del previsto

Quando GlobalScope è giustificato

JetBrains consente GlobalScope solo in scenari rari: processi in background a livello di applicazione che devono vivere anche dopo la chiusura di tutte le Activity (ad esempio, sincronizzazione dati, analisi). Ma anche in questi casi, è preferibile creare il proprio scope con CoroutineScope(SupervisorJob()).

Raccomandazione

Utilizza sempre un CoroutineScope personalizzato con gestione esplicita del ciclo di vita. In Android, questi sono viewModelScope e lifecycleScope. Nelle applicazioni server, crea uno scope per ogni richiesta o pool di connessioni.

coroutineScope vs supervisorScope: qual è la differenza

Entrambe le funzioni sono funzioni suspend che creano uno scope temporaneo per attività parallele, ma il loro comportamento con le eccezioni differisce fondamentalmente.

CaratteristicacoroutineScopesupervisorScope
Comportamento in caso di erroreUn'eccezione in una coroutine figlia cancella tutte le altreUn'eccezione in una coroutine figlia NON cancella le altre
Propagazione dell'erroreSì, la prima eccezione viene propagata all'esternoSì, la prima eccezione viene propagata all'esterno
Job predefinitoJob() — i figli sono legati al padreSupervisorJob() — i figli non dipendono l'uno dall'altro
Caso d'uso tipicoOperazione atomica a più passaggiAttività parallele indipendenti (caricamenti UI)

Quando scegliere coroutineScope

Utilizza coroutineScope quando più operazioni parallele formano un'unica operazione atomica. Ad esempio, caricare dati da tre server: se una richiesta fallisce, le altre non hanno senso.

kotlin
suspend fun loadProductPage(): ProductPage = coroutineScope {
    val product = async { api.getProduct() }
    val reviews = async { api.getReviews() }
    ProductPage(product.await(), reviews.await())
}

Se getProduct o getReviews lancia un'eccezione — entrambe le coroutine vengono cancellate e l'eccezione viene propagata al codice chiamante.

Quando scegliere supervisorScope

Utilizza supervisorScope quando le operazioni parallele non dipendono l'una dall'altra. Ad esempio, caricare i dati del profilo in più sezioni indipendenti: se la sezione dei consigli fallisce, l'intestazione del profilo e l'elenco degli amici dovrebbero essere visualizzati.

Errori comuni con CoroutineScope

Esaminiamo gli errori più comuni degli sviluppatori quando usano CoroutineScope in Kotlin.

Errore 1: Dimenticare di cancellare lo scope

Lo scenario più comune di perdita di coroutine è creare uno scope senza chiamare cancel quando il componente termina. Se lo scope non viene cancellato, le coroutine continuano a essere eseguite, mantenendo riferimenti agli oggetti. In Android, utilizza viewModelScope o lifecycleScope, che vengono cancellati automaticamente.

Errore 2: Usare GlobalScope in Activity o Fragment

GlobalScope ignora il ciclo di vita dei componenti Android. Una coroutine lanciata in GlobalScope dopo la chiusura di un'Activity continuerà a essere eseguita e tenterà di aggiornare l'UI — causando un crash. Utilizza sempre lifecycleScope per i componenti UI.

Errore 3: Riutilizzare uno scope cancellato

Dopo aver chiamato cancel(), lo scope non può essere riutilizzato — tutte le coroutine al suo interno sono già state completate. Crea una nuova istanza di CoroutineScope tramite la funzione factory. Job() non supporta la riattivazione.

Errore 4: Delega errata dell'interfaccia CoroutineScope

Delegando con by, la classe ottiene un metodo cancel() pubblico che può essere chiamato da qualsiasi punto, rompendo l'incapsulamento. Memorizza lo scope come campo privato invece di delegare l'interfaccia.

Domande frequenti

Qual è la differenza tra CoroutineScope e CoroutineContext?

CoroutineScope è un'interfaccia che possiede un CoroutineContext ed è responsabile del ciclo di vita delle coroutine. CoroutineContext è un insieme di elementi (dispatcher, job, gestore di errori) che definisce “come” una coroutine viene eseguita. Una differenza: lo scope crea coroutine, mentre il contesto controlla il loro comportamento.

Si può creare un CoroutineScope con SupervisorJob?

Sì, è un modello standard: CoroutineScope(Dispatchers.IO + SupervisorJob()). SupervisorJob previene la cancellazione a cascata delle coroutine figlie quando una di esse lancia un'eccezione. Questo è utile per attività parallele indipendenti dove un errore in una non dovrebbe fermare le altre.

Quante coroutine può contenere un CoroutineScope?

Non c'è limite al numero di coroutine in uno scope — sono limitate solo dalla memoria disponibile e dalle impostazioni del dispatcher. Il limite pratico è tipicamente di migliaia di coroutine attive in un singolo scope. Tuttavia, un gran numero di coroutine può indicare problemi architetturali.

Come testare il codice con CoroutineScope?

Il modo corretto è passare lo scope alla classe tramite il costruttore o utilizzare runBlockingTest / runTest da kotlinx-coroutines-test. Nei test, puoi sostituire lo scope con TestCoroutineDispatcher e controllare manualmente l'esecuzione delle coroutine.

Una coroutine può avere il proprio scope?

No, uno scope è un contenitore esterno per una coroutine. La coroutine stessa non è uno scope. Tuttavia, all'interno di una coroutine puoi creare un nuovo scope tramite coroutineScope o supervisorScope per lanciare coroutine figlie in parallelo.

Riepilogo

  • CoroutineScope — un'interfaccia con il campo coroutineContext, che definisce il ciclo di vita delle coroutine lanciate al suo interno
  • Concorrenza strutturata — cancellare lo scope cancella automaticamente tutte le coroutine figlie, prevenendo perdite di memoria
  • Job e SupervisorJob — due modalità di gestione degli errori: cancellazione a cascata (Job) ed errori isolati (SupervisorJob)
  • GlobalScope — non raccomandato per la produzione per mancanza di legame con il ciclo di vita
  • coroutineScope vs supervisorScope — operazioni parallele atomiche vs attività parallele indipendenti
  • viewModelScope e lifecycleScope — scope pronti per Android, cancellati automaticamente al termine del componente
  • Funzione factory — il modo preferito per creare uno scope tramite CoroutineContext + chiamata esplicita a cancel

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