Le coroutine Kotlin consentono di scrivere codice asincrono pulito e semplificato che mantiene la tua app reattiva e gestisce al contempo attività a lunga esecuzione come chiamate di rete o operazioni sul disco.
Questo argomento offre un'analisi dettagliata delle coroutine su Android. Se non conosci le coroutine, assicurati di leggere Kotlin coroutine su Android prima di leggere questo argomento.
Gestisci attività di lunga durata
Le coroutine si basano su funzioni regolari aggiungendo due operazioni per gestire attività di lunga durata. Oltre a invoke
(o call
) e return
,
le coroutine aggiungono suspend
e resume
:
suspend
mette in pausa l'esecuzione della coroutine corrente, salvando tutte le variabili locali.resume
continua l'esecuzione di una coroutine sospesa dal luogo in cui è stata sospesa.
Puoi chiamare le funzioni suspend
solo da altre funzioni suspend
o
utilizzando un generatore di coroutine come launch
per avviare una nuova coroutine.
L'esempio seguente mostra una semplice implementazione di coroutine per un'ipotetica attività a lunga esecuzione:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
In questo esempio, get()
continua a essere eseguito sul thread principale, ma sospende la coroutine prima di avviare la richiesta di rete. Quando la richiesta di rete
viene completata, get
ripristina la coroutine sospesa anziché utilizzare un callback
per inviare una notifica al thread principale.
Kotlin utilizza un frame in stack per gestire la funzione in esecuzione insieme a qualsiasi variabile locale. Quando sospendi una coroutine, lo stack frame corrente viene copiato e salvato per un secondo momento. Al ripristino, lo stack frame viene copiato dalla posizione in cui era stato salvato e la funzione viene nuovamente eseguita. Anche se il codice potrebbe sembrare una normale richiesta di blocco sequenziale, la coroutine assicura che la richiesta di rete eviti di bloccare il thread principale.
Usa le coroutine per la sicurezza principale
Le coroutine Kotlin utilizzano i corrieri per determinare quali thread vengono utilizzati per l'esecuzione delle coroutine. Per eseguire il codice al di fuori del thread principale, puoi chiedere a Kotlin coroutine di eseguire il lavoro sul supervisore predefinito o IO. In Kotlin, tutte le coroutine devono essere eseguite in un supervisore, anche quando sono in esecuzione sul thread principale. Le coroutine possono sospendersi da sole e il supervisore sarà responsabile di ripristinarle.
Per specificare dove devono essere eseguite le coroutine, Kotlin offre tre supervisori che puoi utilizzare:
- Dispatchers.Main: utilizza questo supervisore per eseguire una coroutine sul thread
Android principale. Deve essere utilizzata solo per interagire con l'interfaccia utente ed eseguire operazioni rapide. Gli esempi includono la chiamata alle funzioni
suspend
, l'esecuzione di operazioni del framework dell'interfaccia utente di Android e l'aggiornamento degli oggettiLiveData
. - Dispatchers.IO: questo supervisore è ottimizzato per eseguire I/O su disco o rete al di fuori del thread principale. Alcuni esempi sono l'utilizzo del componente Room, la lettura o la scrittura su file e l'esecuzione di qualsiasi operazione di rete.
- Dispatchers.Default: questo supervisore è ottimizzato per eseguire il lavoro ad alta intensità di CPU al di fuori del thread principale. Esempi di casi d'uso includono l'ordinamento di un elenco e l'analisi del codice JSON.
Continuando con l'esempio precedente, puoi utilizzare i supervisori per ridefinire la funzione get
. All'interno del corpo di get
, chiama withContext(Dispatchers.IO)
per
creare un blocco che viene eseguito sul pool di thread di I/O. Qualsiasi codice inserito nel blocco
viene sempre eseguito tramite il supervisore IO
. Poiché withContext
è a sua volta una funzione di sospensione, anche la funzione get
è una funzione di sospensione.
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
Con le coroutine, puoi inviare thread con un controllo granulare. Poiché withContext()
ti consente di controllare il pool di thread di qualsiasi riga di codice senza introdurre callback, puoi applicarlo a funzioni molto piccole come la lettura da un database o l'esecuzione di una richiesta di rete. È buona norma usare withContext()
per assicurarti che ogni funzione sia main-safe, il che significa che puoi chiamare la funzione dal thread principale. In questo modo il chiamante non deve mai pensare a quale thread deve essere utilizzato per eseguire la funzione.
Nell'esempio precedente, fetchDocs()
viene eseguito sul thread principale. Tuttavia, può chiamare in sicurezza get
, che esegue una richiesta di rete in background.
Poiché le coroutine supportano suspend
e resume
, la coroutine nel thread principale viene ripresa con il risultato get
non appena viene completato il blocco withContext
.
Rendimento di withContext()
withContext()
non comporta un overhead aggiuntivo rispetto a un'implementazione equivalente basata
su callback. Inoltre, in alcune situazioni è possibile ottimizzare le chiamate withContext()
al di là di un'implementazione equivalente basata su callback. Ad esempio, se una funzione effettua dieci chiamate a una rete, puoi dire a Kotlin di cambiare thread solo una volta utilizzando un withContext()
esterno. Quindi, anche se la libreria di rete utilizza withContext()
più volte, rimane sullo stesso mittente ed evita di cambiare thread. Inoltre, Kotlin ottimizza il passaggio tra Dispatchers.Default
e Dispatchers.IO
per evitare il cambio dei thread quando possibile.
Avvia una coroutine
Puoi avviare una coroutine in due modi:
launch
avvia una nuova coroutine e non restituisce il risultato al chiamante. Qualsiasi opera considerata "fuoco e dimentica" può essere avviata utilizzandolaunch
.async
avvia una nuova coroutine e ti consente di restituire un risultato con una funzione di sospensione chiamataawait
.
In genere, dovresti launch
una nuova coroutine da una funzione regolare, poiché una funzione regolare non può chiamare await
. Utilizza async
solo quando all'interno di un'altra coroutine o all'interno di una funzione di sospensione ed esegui la decomposizione parallela.
Decomposizione parallela
Tutte le coroutine avviate all'interno di una funzione suspend
devono essere interrotte quando viene restituita la funzione, quindi è probabile che tu debba garantire che le coroutine terminino prima di tornare. Con la contemporaneità strutturata in Kotlin, puoi definire
un coroutineScope
che avvia una o più coroutine. Quindi, utilizzando await()
(per una singola coroutine) o awaitAll()
(per più coroutine), puoi
garantire che queste coroutine terminino prima di tornare dalla funzione.
Ad esempio, definiamo un valore coroutineScope
che recupera due documenti
in modo asincrono. Richiamando await()
su ogni riferimento differito, garantiamo il completamento di entrambe le operazioni di async
prima di restituire un valore:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
Puoi anche usare awaitAll()
nelle raccolte, come mostrato nell'esempio seguente:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
Anche se fetchTwoDocs()
lancia nuove coroutine con async
, la funzione
utilizza awaitAll()
per attendere il termine di quelle avviate prima
di tornare. Tuttavia, tieni presente che, anche se non avessimo chiamato awaitAll()
, lo strumento per la creazione di coroutineScope
non riprende la coroutine che ha chiamato fetchTwoDocs
fino al completamento di tutte le nuove coroutine.
Inoltre, coroutineScope
individua eventuali eccezioni generate dalle coroutine
e le reindirizza al chiamante.
Per ulteriori informazioni sulla decomposizione parallela, consulta Composizione di funzioni di sospensione.
Concetti sulle coroutine
CoroutineScope
Un CoroutineScope
tiene traccia di qualsiasi coroutine creata utilizzando launch
o async
. Il
lavoro in corso (ovvero le coroutine in corso) può essere annullato chiamando
scope.cancel()
in qualsiasi momento. In Android, alcune librerie KTX forniscono
il proprio CoroutineScope
per determinate classi del ciclo di vita. Ad esempio, ViewModel
ha viewModelScope
e Lifecycle
ha lifecycleScope
.
A differenza di un supervisore, tuttavia, un CoroutineScope
non esegue le coroutine.
viewModelScope
è utilizzato anche negli esempi che si trovano in Threading in background su Android con Coroutine.
Tuttavia, se devi creare un CoroutineScope
personalizzato per controllare il ciclo di vita delle coroutine in un determinato livello dell'app, puoi crearne uno nel seguente modo:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
Un ambito annullato non può creare altre coroutine. Pertanto, devi chiamare scope.cancel()
solo quando viene eliminata la classe che controlla il suo ciclo di vita. Quando utilizzi viewModelScope
, la classe ViewModel
annulla automaticamente l'ambito nel metodo onCleared()
di ViewModel.
Job
Un Job
è l'handle di una coroutine. Ogni coroutine creata con launch
o async
restituisce un'istanza Job
che identifica in modo univoco la
coroutine e ne gestisce il ciclo di vita. Puoi anche passare un Job
a un CoroutineScope
per gestirne ulteriormente il ciclo di vita, come illustrato nell'esempio seguente:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
Un elemento CoroutineContext
definisce il comportamento di una coroutina utilizzando il seguente insieme di elementi:
Job
: controlla il ciclo di vita della coroutine.CoroutineDispatcher
: I supervisori vengono indirizzati al thread appropriato.CoroutineName
: il nome della coroutine, utile per il debug.CoroutineExceptionHandler
: gestisce le eccezioni non rilevate.
Per le nuove coroutine create all'interno di un ambito, viene assegnata una nuova istanza Job
alla nuova coroutine, mentre gli altri elementi CoroutineContext
vengono ereditati dall'ambito contenitore. Puoi eseguire l'override degli elementi ereditati passando un nuovo valore CoroutineContext
alla funzione launch
o async
. Tieni presente che il passaggio di Job
a launch
o async
non ha alcun effetto, poiché una nuova istanza di Job
viene sempre assegnata a una nuova coroutine.
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Risorse aggiuntive sulle coroutine
Per altre risorse sulle coroutine, consulta i seguenti link: