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 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.
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.
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.
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.
Comprendere il funzionamento interno di CoroutineScope richiede familiarità con il concetto di Job e il principio di concorrenza strutturata.
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:
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:
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.
Puoi creare un CoroutineScope tramite una funzione factory o implementando l'interfaccia nella tua classe. Esaminiamo entrambi gli approcci.
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.
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.
Kotlin consente di delegare l'implementazione di CoroutineScope tramite la parola chiave by:
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 è un singleton CoroutineScope per l'intera applicazione. Il suo utilizzo in codice di produzione non è ufficialmente raccomandato.
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()).
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.
Entrambe le funzioni sono funzioni suspend che creano uno scope temporaneo per attività parallele, ma il loro comportamento con le eccezioni differisce fondamentalmente.
| Caratteristica | coroutineScope | supervisorScope |
|---|---|---|
| Comportamento in caso di errore | Un'eccezione in una coroutine figlia cancella tutte le altre | Un'eccezione in una coroutine figlia NON cancella le altre |
| Propagazione dell'errore | Sì, la prima eccezione viene propagata all'esterno | Sì, la prima eccezione viene propagata all'esterno |
| Job predefinito | Job() — i figli sono legati al padre | SupervisorJob() — i figli non dipendono l'uno dall'altro |
| Caso d'uso tipico | Operazione atomica a più passaggi | Attività parallele indipendenti (caricamenti UI) |
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.
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.
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.
Esaminiamo gli errori più comuni degli sviluppatori quando usano CoroutineScope in Kotlin.
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.
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.
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.
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
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.
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.
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.
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.
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
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