Le coroutine Kotlin ti consentono di scrivere codice asincrono pulito e semplificato che mantiene la tua app reattiva durante la gestione di attività a lunga esecuzione come chiamate di rete o operazioni su disco.
Questo argomento fornisce una panoramica dettagliata delle coroutine su Android. Se non hai familiarità con le coroutine, assicurati di leggere Coroutine Kotlin su Android prima di leggere questo argomento.
Gestire le attività di lunga durata
Le coroutine si basano sulle funzioni regolari aggiungendo due operazioni per gestire
le attività di lunga durata. Oltre a invoke (o call) e return,
le coroutine aggiungono suspend e resume:
suspendmette in pausa l'esecuzione della coroutine corrente, salvando tutte le variabili locali.resumecontinua l'esecuzione di una coroutine sospesa dal punto in cui è stata sospesa.
Puoi chiamare le funzioni suspend solo da altre funzioni suspend o
utilizzando un builder di coroutine come launch per avviare una nuova coroutine.
Il seguente esempio mostra una semplice implementazione di coroutine per un'ipotetica attività di lunga durata:
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() viene ancora eseguito sul thread principale, ma sospende la
coroutine prima di avviare la richiesta di rete. Al termine della richiesta di rete, get riprende la coroutine sospesa anziché utilizzare un callback per notificare il thread principale.
Kotlin utilizza un frame di stack per gestire la funzione in esecuzione insieme a eventuali variabili locali. Quando una coroutine viene sospesa, il frame dello stack corrente viene copiato e salvato per un secondo momento. Quando viene ripresa, il frame dello stack viene ricopiato da dove era stato salvato e la funzione riprende l'esecuzione. Anche se il codice potrebbe sembrare una normale richiesta di blocco sequenziale, la coroutine garantisce che la richiesta di rete eviti di bloccare il thread principale.
Utilizzare le coroutine per la sicurezza del thread principale
Le coroutine Kotlin utilizzano i dispatcher per determinare quali thread vengono utilizzati per l'esecuzione delle coroutine. Per eseguire il codice al di fuori del thread principale, puoi indicare alle coroutine Kotlin di eseguire il lavoro sul dispatcher Default o IO. In Kotlin, tutte le coroutine devono essere eseguite in un dispatcher, anche quando vengono eseguite sul thread principale. Le coroutine possono sospendersi e il dispatcher è responsabile della loro ripresa.
Per specificare dove devono essere eseguite le coroutine, Kotlin fornisce tre dispatcher che puoi utilizzare:
- Dispatchers.Main: utilizza questo dispatcher per eseguire una coroutine sul thread principale di Android. Deve essere utilizzato solo per interagire con la UI ed eseguire operazioni rapide. Gli esempi includono la chiamata di funzioni
suspend, l'esecuzione di operazioni del framework UI di Android e l'aggiornamento di oggettiLiveData. - Dispatchers.IO: questo dispatcher è ottimizzato per eseguire I/O di disco o di rete al di fuori del thread principale. Alcuni esempi includono l'utilizzo del componente Room, la lettura o la scrittura di file e l'esecuzione di qualsiasi operazione di rete.
- Dispatchers.Default: questo dispatcher è ottimizzato per eseguire attività che richiedono un uso intensivo della CPU al di fuori del thread principale. Tra i casi d'uso di esempio ci sono l'ordinamento di un elenco e l'analisi JSON.
Continuando l'esempio precedente, puoi utilizzare i dispatcher per ridefinire la funzione get. All'interno del corpo di get, chiama withContext(Dispatchers.IO) per
creare un blocco che viene eseguito nel pool di thread I/O. Qualsiasi codice inserito all'interno di questo blocco viene sempre eseguito tramite il dispatcher IO. Poiché withContext è 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 distribuire i 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. Una buona pratica è utilizzare
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 sul thread principale viene ripresa con il risultato get non appena il blocco withContext è terminato.
Rendimento di withContext()
withContext()
non aggiunge un sovraccarico aggiuntivo rispetto a un'implementazione
equivalente basata su callback. Inoltre, in alcune situazioni è possibile ottimizzare le chiamate withContext()
oltre a un'implementazione equivalente basata sul richiamo. Ad esempio, se una funzione effettua dieci chiamate a una rete, puoi indicare a Kotlin di cambiare thread una sola volta utilizzando un withContext() esterno. In questo modo, anche se
la libreria di rete utilizza withContext() più volte, rimane sullo stesso
dispatcher ed evita di cambiare thread. Inoltre, Kotlin ottimizza il passaggio
tra Dispatchers.Default e Dispatchers.IO per evitare cambi di thread
ogni volta che è possibile.
Avvia una coroutine
Puoi avviare le coroutine in due modi:
launchavvia una nuova coroutine e non restituisce il risultato al chiamante. Qualsiasi lavoro considerato "fire and forget" può essere avviato utilizzandolaunch.asyncavvia una nuova coroutine e ti consente di restituire un risultato con una funzione di sospensione chiamataawait.
In genere, devi launch una nuova coroutine da una funzione normale, poiché una funzione normale non può chiamare await. Utilizza async solo all'interno
di un'altra coroutine o di una funzione di sospensione ed esegui
la scomposizione parallela.
Decomposizione parallela
Tutte le coroutine avviate all'interno di una funzione suspend devono essere interrotte quando la funzione restituisce un valore, quindi probabilmente devi assicurarti che queste coroutine terminino prima di restituire un valore. Con la concorrenza strutturata in Kotlin, puoi definire
un coroutineScope che avvia una o più coroutine. Poi, utilizzando await()
(per una singola coroutine) o awaitAll() (per più coroutine), puoi
garantire che queste coroutine terminino prima di restituire un valore dalla funzione.
Ad esempio, definiamo un coroutineScope che recupera due documenti
in modo asincrono. Chiamando await() su ogni riferimento posticipato, garantiamo
che entrambe le operazioni async terminino 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 utilizzare 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() avvia nuove coroutine con async, la funzione
utilizza awaitAll() per attendere il completamento delle coroutine avviate prima
di restituire un valore. Tieni presente, tuttavia, che anche se non avessimo chiamato awaitAll(), il
generatore coroutineScope non riprende la coroutine che ha chiamato
fetchTwoDocs finché tutte le nuove coroutine non sono state completate.
Inoltre, coroutineScope rileva eventuali eccezioni generate dalle coroutine
e le reindirizza al chiamante.
Per saperne di più sulla scomposizione 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 esecuzione) 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 un
viewModelScope,
e Lifecycle ha lifecycleScope.
A differenza di un dispatcher, tuttavia, un CoroutineScope non esegue le coroutine.
viewModelScope viene utilizzato anche negli esempi riportati in
Background threading on Android with Coroutines.
Tuttavia, se devi creare il tuo CoroutineScope per controllare il
ciclo di vita delle coroutine in un particolare livello della tua app, puoi crearne uno
come segue:
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 la classe che controlla il suo ciclo di vita
viene eliminata. Quando utilizzi viewModelScope, la classe
ViewModel annulla automaticamente
l'ambito nel metodo onCleared() di ViewModel.
Job
Un Job
è un 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 mostrato nell'esempio seguente:
class ExampleClass { // ... fun exampleMethod() { // Handle to the coroutine, you can control its lifecycle val job = scope.launch { // New coroutine } if (condition) { // Cancel the coroutine started above, this doesn't affect the scope // this coroutine was launched in job.cancel() } } }
CoroutineContext
Un CoroutineContext
definisce il comportamento di una coroutine utilizzando il seguente insieme di elementi:
Job: controlla il ciclo di vita della coroutine.CoroutineDispatcher: Distribuisce il lavoro 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, alla nuova coroutine viene assegnata una nuova istanza di Job e gli altri elementi CoroutineContext vengono ereditati dall'ambito contenitore. Puoi eseguire l'override degli elementi ereditati passando un nuovo CoroutineContext alla funzione launch o async. Tieni presente che il passaggio di un Job a launch o async non ha effetto,
in quanto 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 ulteriori risorse sulle coroutine, consulta i seguenti link: