In questo codelab imparerai a utilizzare Kotlin Coroutines in un'app Android, un nuovo modo di gestire i thread in background che possono semplificare il codice riducendo la necessità di richiamarli. Le Coroutine sono una funzionalità di Kotlin che converte i callback asincroni per le attività di lunga durata, come l'accesso a database o reti, in codice sequenziale.
Ecco uno snippet di codice per darti un'idea di quello che farai.
// Async callbacks
networkRequest { result ->
// Successful network request
databaseSave(result) { rows ->
// Result saved
}
}
Il codice basato su callback verrà convertito in codice sequenziale utilizzando le coroutine.
// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved
Inizierai con un'app esistente, creata utilizzando Componenti dell'architettura, che utilizza uno stile di callback per le attività di lunga durata.
Alla fine di questo codelab avrai esperienza sufficiente a utilizzare le coroutine nella tua app per caricare i dati dalla rete e potrai integrare le coroutine in un'app. Conoscerai anche le best practice per le coroutine e come scrivere un test rispetto al codice che le utilizza.
Prerequisiti
- Familiarità con i componenti dell'architettura
ViewModel
,LiveData
,Repository
eRoom
. - Esperienza con la sintassi Kotlin, incluse funzioni di estensione e lambda.
- Conoscenza di base dell'utilizzo dei thread su Android, compresi il thread principale, i thread in background e le richiamate.
Attività previste
- Codice di chiamata scritto con coroutine e ottenuto i risultati.
- Utilizza le funzioni di sospensione per rendere sequenziale il codice asincrono.
- Utilizza i criteri
launch
erunBlocking
per controllare la modalità di esecuzione del codice. - Scopri le tecniche per convertire le API esistenti in coroutine utilizzando
suspendCoroutine
. - Usare coroutine con i componenti dell'architettura.
- Scopri le best practice per il test delle coroutine.
Che cosa ti serve
- Android Studio 3.5 (il codelab potrebbe funzionare con altre versioni, ma alcune cose potrebbero mancare o avere un aspetto diverso).
Se riscontri problemi (bug di codice, errori grammaticali, parole non chiare e così via) via email con questo codelab, segnala il problema tramite il link Segnala un errore nell'angolo in basso a sinistra del codelab.
Scarica il codice
Fai clic sul seguente link per scaricare tutto il codice di questo codelab:
... oppure clona il repository GitHub dalla riga di comando utilizzando il seguente comando:
$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git
Domande frequenti
Innanzitutto, vediamo come appare l'app di esempio iniziale. Segui queste istruzioni per aprire l'app di esempio in Android Studio.
- Se hai scaricato il file ZIP
kotlin-coroutines
, decomprimi il file. - Apri il progetto
coroutines-codelab
in Android Studio. - Seleziona il modulo dell'applicazione
start
. - Fai clic sul pulsante Esegui e scegli un emulatore oppure collega il tuo dispositivo Android, che deve supportare Android Lollipop (l'SDK minimo supportato è 21). Dovrebbe apparire la schermata Kotlin Coroutines:
Questa app iniziale utilizza i thread per aumentare il conteggio di un breve ritardo dopo la pressione dello schermo. nonché recuperare un nuovo titolo dalla rete e visualizzarlo sullo schermo. Prova subito. Dovresti notare che il numero e il messaggio cambiano dopo un breve ritardo. In questo codelab, convertirai questa applicazione in modo che utilizzi coroutine.
Questa app utilizza i componenti dell'architettura per separare il codice dell'interfaccia utente in MainActivity
dalla logica dell'applicazione in MainViewModel
. Dedica un momento a definire la struttura del progetto.
MainActivity
mostra l'interfaccia utente, registra i listener di clic e può mostrare unSnackbar
. Passa gli eventi aMainViewModel
e aggiorna lo schermo in base aLiveData
inMainViewModel
.MainViewModel
gestisce gli eventi inonMainViewClicked
e comunicherà aMainActivity
utilizzandoLiveData.
Executors
definisceBACKGROUND,
che può eseguire elementi su un thread in background.TitleRepository
recupera i risultati dalla rete e li salva nel database.
Aggiungere coroutine a un progetto
Per utilizzare le coroutine in Kotlin, devi includere la libreria coroutines-core
nel file build.gradle (Module: app)
del progetto. I progetti del codelab sono già riusciti a farlo per te, quindi non devi farlo per completare il codelab.
Coroutines su Android sono disponibili come raccolta principale e estensioni specifiche per Android:
- kotlinx-corountines-core : interfaccia principale per l'utilizzo di coroutine in Kotlin
- kotlinx-coroutines-android: supporto per il thread principale di Android in coroutine.
L'app iniziale include già le dipendenze in build.gradle.
Quando crei un nuovo progetto di app, devi aprire build.gradle (Module: app)
e aggiungere le dipendenze coroutine al progetto.
dependencies { ... implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x" }
Su Android, è essenziale evitare di bloccare il thread principale. Il thread principale è un singolo thread che gestisce tutti gli aggiornamenti all'interfaccia utente. È anche il thread che chiama tutti i gestori dei clic e altri callback di interfaccia utente. Di conseguenza, deve funzionare senza problemi per garantire un'ottima esperienza utente.
Per fare in modo che l'app venga mostrata agli utenti senza pause visibili, il thread principale deve aggiornare lo schermo ogni 16 ms o più, ovvero circa 60 frame al secondo. Molte attività comuni richiedono più tempo, ad esempio l'analisi di grandi set di dati JSON, la scrittura di dati in un database o il recupero di dati dalla rete. Pertanto, chiamate codice come questo dal thread principale può mettere in pausa l'app, interromperla o bloccarla. Se blocchi il thread principale per troppo tempo, l'app potrebbe persino arrestarsi in modo anomalo e presentare una finestra di dialogo L'applicazione non risponde.
Guarda il video di seguito per un'introduzione al modo in cui le coroutine risolvono il problema su Android utilizzando la sicurezza principale.
La sequenza di callback
Un pattern per eseguire attività di lunga durata senza bloccare il thread principale è costituito dai callback. Utilizzando i callback, puoi avviare attività di lunga durata in un thread in background. Una volta completata l'attività, viene richiamato il callback per informarti del risultato nel thread principale.
Dai un'occhiata a un esempio di pattern per il callback.
// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
// The slow network request runs on another thread
slowFetch { result ->
// When the result is ready, this callback will get the result
show(result)
}
// makeNetworkRequest() exits after calling slowFetch without waiting for the result
}
Poiché questo codice è annotato con @UiThread
, deve essere abbastanza veloce da essere eseguito nel thread principale. Ciò significa che deve tornare molto velocemente, in modo che il prossimo aggiornamento della schermata non subisca ritardi. Tuttavia, poiché slowFetch
impiegherà pochi secondi o addirittura minuti per essere completato, il thread principale non può attendere il risultato. Il callback show(result)
consente a slowFetch
di essere eseguito su un thread in background e di restituire il risultato quando è pronto.
Utilizzare le coroutine per rimuovere i callback
I richiami sono un ottimo schema, ma offrono alcuni svantaggi. Il codice che fa un uso intensivo dei callback può diventare difficile da leggere e da valutare. Inoltre, i callback non consentono l'utilizzo di alcune funzionalità in lingua, come le eccezioni.
Le coroutine Kotlin consentono di convertire il codice basato su callback in un codice sequenziale. Il codice scritto in sequenza è generalmente più facile da leggere e può persino utilizzare funzionalità linguistiche come le eccezioni.
Alla fine i clienti lavorano esattamente allo stesso modo: attendi che sia disponibile un risultato per un'attività a lunga esecuzione e continua con l'esecuzione. Tuttavia, nel codice sono molto diversi.
La parola chiave suspend
è un metodo di Kotlin per contrassegnare una funzione, o tipo di funzione, disponibile per le coroutine. Quando una funzione di chiamata chiama una funzione contrassegnata con la dicitura suspend
, invece di bloccarla finché una funzione non restituisce una chiamata di funzione normale, sospende l'esecuzione fino a quando il risultato non è pronto, quindi ripristina il punto in cui era stato interrotto. Mentre è in attesa di un risultato, il thread sblocca il thread su cui è in esecuzione, in modo da poter eseguire altre funzioni o le coroutine.
Ad esempio, nel codice riportato di seguito, makeNetworkRequest()
e slowFetch()
sono entrambe funzioni suspend
.
// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
// slowFetch is another suspend function so instead of
// blocking the main thread makeNetworkRequest will `suspend` until the result is
// ready
val result = slowFetch()
// continue to execute after the result is ready
show(result)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
Come nel caso della versione di callback, makeNetworkRequest
deve tornare subito dal thread principale perché è contrassegnato come @UiThread
. Ciò significa che in genere non è possibile chiamare metodi di blocco come slowFetch
. Ecco dove usa la parola chiave suspend
.
Rispetto al codice basato su callback, il codice coroutine consente di ottenere lo stesso risultato dello sblocco del thread corrente con un codice inferiore. Grazie allo stile sequenziale, è facile concatenare varie attività di lunga durata senza dover creare più callback. Ad esempio, il codice che recupera un risultato da due endpoint di rete e lo salva nel database può essere scritto come funzione in coroutine senza callback. In questo modo:
// Request data from network and save it to database with coroutines
// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
// slowFetch and anotherFetch are suspend functions
val slow = slowFetch()
val another = anotherFetch()
// save is a regular function and will block this thread
database.save(slow, another)
}
// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }
Nella sezione successiva presenterai le coroutine all'app di esempio.
In questo esercizio scriverai una coroutine per visualizzare un messaggio dopo un ritardo. Per iniziare, assicurati di avere il modulo start
aperto in Android Studio.
Informazioni su CoroutineScope
In Kotlin, tutte le coroutine si trovano all'interno di una CoroutineScope
. Un ambito controlla la durata delle coroutine attraverso il proprio lavoro. Quando annulli il job di un ambito, vengono annullate tutte le coroutine avviate in tale ambito. Su Android puoi utilizzare un ambito per annullare tutte le coroutine in esecuzione quando, ad esempio, l'utente si allontana da un elemento Activity
o Fragment
. Gli ambiti consentono inoltre di specificare un supervisore predefinito. Un supervisore controlla quale thread esegue una coroutine.
Per le coroutine avviate dall'interfaccia utente, in genere è corretto avviarle su Dispatchers.Main
, il thread principale su Android. Una coroutine avviata il giorno Dispatchers.Main
non bloccherà il thread principale mentre è sospeso. Poiché una coroutine ViewModel
aggiorna quasi sempre l'interfaccia utente del thread principale, l'avvio di coroutine nel thread principale consente di risparmiare meno possibilità di passaggio. Una coroutine avviata nel thread principale può cambiare i compagni in qualsiasi momento dopo l'avvio. Ad esempio, può utilizzare un altro supervisore per analizzare un risultato JSON di grandi dimensioni all'interno del thread principale.
Utilizzo di viewModelScope
Nella libreria AndroidX lifecycle-viewmodel-ktx
viene aggiunto un CoroutineScope ai Viewmodels, configurati per iniziare le coroutine correlate all'interfaccia utente. Per utilizzare questa libreria, devi includerla nel file build.gradle (Module: start)
del progetto. Questo passaggio è già stato eseguito nei progetti codelab.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
La libreria aggiunge una viewModelScope
come funzione di estensione del corso ViewModel
. Questo ambito è associato a Dispatchers.Main
e verrà annullato automaticamente una volta cancellato ViewModel
.
Passare dai thread alle coroutine
In MainViewModel.kt
trova il prossimo TODO insieme a questo codice:
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
BACKGROUND.submit {
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
}
Questo codice utilizza il BACKGROUND ExecutorService
(definito in util/Executor.kt
) per l'esecuzione in un thread in background. Poiché sleep
blocca il thread corrente, l'interfaccia utente si bloccherà se fosse stata chiamata nel thread principale. Un secondo dopo che l'utente fa clic sulla vista principale, richiede uno snackbar.
Puoi vedere che ciò accade rimuovendo il BACKGROUND dal codice ed eseguendolo di nuovo. La rotellina di caricamento non mostra il display e tutto torna allo stato finale un secondo dopo.
MainViewModel.kt
/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
// TODO: Convert updateTaps to use coroutines
tapCount++
Thread.sleep(1_000)
_taps.postValue("$tapCount taps")
}
Sostituisci updateTaps
con questo codice basato su coroutine che funziona allo stesso modo. Dovrai importare launch
e delay
.
MainViewModel.kt
/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
// launch a coroutine in viewModelScope
viewModelScope.launch {
tapCount++
// suspend this coroutine for one second
delay(1_000)
// resume in the main dispatcher
// _snackbar.value can be called directly from main thread
_taps.postValue("$tapCount taps")
}
}
Questo codice funziona allo stesso modo, attendi un secondo prima di mostrare uno snack bar. Esistono tuttavia alcune differenze importanti:
viewModelScope.
launch
inizierà una coroutina nellaviewModelScope
. Ciò significa che il processo che abbiamo trasferito aviewModelScope
verrà annullato, tutte le attività in questo job/ambito verranno annullate. Se l'utente ha lasciato l'attività prima chedelay
sia tornato, questa coroutine verrà annullata automaticamente quando viene richiamatoonCleared
al momento dell'eliminazione di ViewModel.- Poiché
viewModelScope
ha un supervisore predefinito diDispatchers.Main
, questa stringa verrà avviata nel thread principale. Più avanti vedremo come utilizzare diversi thread. - La funzione
delay
è una funzionesuspend
. Su Android Studio viene mostrata l'icona nella grondaia a sinistra. Anche se questa stringa è in esecuzione sul thread principale,delay
non la bloccherà per un secondo. Invece, il supervisore programma la ripresa della coroutine tra un secondo alla prossima dichiarazione.
Eseguila. Quando fai clic sulla vista principale, dovresti vedere uno snackbar in un secondo momento.
Nella sezione successiva vedremo come testare questa funzione.
In questo esercizio scriverai un test per il codice che hai appena scritto. Questo esercizio mostra come testare le coroutine in esecuzione su Dispatchers.Main
utilizzando la libreria kotlinx-coroutines-test. Più avanti in questo codelab, implementerai un test che interagisce direttamente con le coroutine.
Esamina il codice esistente
Apri MainViewModelTest.kt
nella cartella androidTest
.
MainViewModelTest.kt
class MainViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var subject: MainViewModel
@Before
fun setup() {
subject = MainViewModel(
TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("initial")
))
}
}
Una regola è un modo per eseguire il codice prima e dopo l'esecuzione di un test in JUnit. In un test esterno al dispositivo vengono utilizzate due regole per testare Test.
InstantTaskExecutorRule
è una regola JUnit che configuraLiveData
per l'esecuzione sincrona di ogni attivitàMainCoroutineScopeRule
è una regola personalizzata in questo codebase che configuraDispatchers.Main
in modo che utilizzi unaTestCoroutineDispatcher
dakotlinx-coroutines-test
. Questo consente ai test di avanzare un orologio virtuale per i test e consente al codice di utilizzareDispatchers.Main
nei test delle unità.
Nel metodo setup
, viene creata una nuova istanza di MainViewModel
utilizzando test falsi, ovvero implementazioni false della rete e del database forniti nel codice di avvio per aiutare a scrivere test senza utilizzare la rete o il database reali.
Per questo test, i falsi sono necessari solo per soddisfare le dipendenze di MainViewModel
. Più avanti in questo codelab, aggiorneremo i falsi per supportare le coroutine.
Scrivere un test che controlli le coroutine
Aggiungi un nuovo test che garantisca l'aggiornamento dei tocchi un secondo dopo il clic sulla visualizzazione principale:
MainViewModelTest.kt
@Test
fun whenMainClicked_updatesTaps() {
subject.onMainViewClicked()
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
coroutineScope.advanceTimeBy(1000)
Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}
Chiamata a onMainViewClicked
per avviare le coroutine appena create. Questo test controlla che il testo dei tocchi rimanga "da 0 tocchi subito dopo aver chiamato onMainViewClicked
, mentre dopo 1 secondo viene aggiornato a e 1 tocco.
Questo test utilizza il tempo virtuale per controllare l'esecuzione della coroutine lanciata da onMainViewClicked
. MainCoroutineScopeRule
consente di mettere in pausa, riprendere o controllare l'esecuzione di coroutine lanciate su Dispatchers.Main
. Qui chiamiamo advanceTimeBy(1_000)
, che farà sì che il supervisore principale esegua immediatamente le coroutine di cui è pianificato il ripristino un secondo.
Questo test è completamente deterministico, il che significa che verrà eseguito sempre allo stesso modo. Inoltre, poiché ha il pieno controllo sull'esecuzione delle coroutine lanciate su Dispatchers.Main
, non deve attendere un secondo affinché il valore venga impostato.
Esegui il test esistente
- Fai clic con il pulsante destro del mouse sul nome del corso
MainViewModelTest
nell'editor per aprire un menu contestuale. - Nel menu contestuale scegli Esegui 'MainViewmodelTest'
- Per le esecuzioni future, puoi selezionare questa configurazione di test nelle configurazioni accanto al pulsante della barra degli strumenti. Per impostazione predefinita, la configurazione sarà denominata MainViewModelTest.
Dovresti vedere il pass di prova. L'esecuzione dovrebbe richiedere poco meno di un secondo.
Nel prossimo esercizio imparerai a eseguire la conversione da API di callback esistenti a coroutine.
In questo passaggio inizierai a convertire un repository per utilizzare le coroutine. A questo scopo, aggiungeremo le coroutine a ViewModel
, Repository
, Room
e Retrofit
.
Prima di passare all'utilizzo di coroutine, è buona norma comprendere in cosa consiste la responsabilità di ogni parte dell'architettura.
MainDatabase
implementa un database utilizzando Room che salva e carica unTitle
.MainNetwork
implementa un'API di rete che recupera un nuovo titolo. Usa Retrofit per recuperare i titoli. L'appRetrofit
è configurata in modo da restituire in modo casuale errori o dati fittizi, ma in caso contrario si comporta come se fosse in possesso di richieste di rete reali.TitleRepository
implementa una singola API per recuperare o aggiornare il titolo combinando i dati della rete e del database.MainViewModel
rappresenta lo stato dello schermo e gestisce gli eventi. Comunica al repository di aggiornare il titolo quando l'utente tocca lo schermo.
Dal momento che la richiesta di rete è basata su eventi interfaccia utente e vogliamo avviare una coroutine sulla base di questi, il punto naturale per iniziare a utilizzare le coroutine è in ViewModel
.
La versione di callback
Apri MainViewModel.kt
per vedere la dichiarazione di refreshTitle
.
MainViewModel.kt
/**
* Update title text via this LiveData
*/
val title = repository.title
// ... other code ...
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
// TODO: Convert refreshTitle to use coroutines
_spinner.value = true
repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
override fun onCompleted() {
_spinner.postValue(false)
}
override fun onError(cause: Throwable) {
_snackBar.postValue(cause.message)
_spinner.postValue(false)
}
})
}
Questa funzione viene chiamata ogni volta che l'utente fa clic sullo schermo e fa sì che il repository aggiorni il titolo e scriva il nuovo titolo nel database.
Questa implementazione utilizza un callback per eseguire alcune operazioni:
- Prima di avviare una query, mostra una rotellina di caricamento con
_spinner.value = true
- Quando riceve un risultato, viene mostrata la rotellina di caricamento con la
_spinner.value = false
- Se viene mostrato un errore, lo snack bar mostra e mostra la rotellina
Tieni presente che il callback onCompleted
non ha superato il title
. Poiché scriviamo tutti i titoli nel database Room
, l'interfaccia utente si aggiorna al titolo corrente osservando un LiveData
aggiornato da Room
.
Nell'aggiornamento delle coroutine, manterremo lo stesso comportamento. È un buon modello per utilizzare un'origine dati osservabile come un database Room
per mantenere automaticamente aggiornata l'interfaccia utente.
La versione delle coroutine
Riscrivi refreshTitle
con le coroutine!
Poiché ne avremo bisogno subito, creiamo una funzione di sospensione vuota nel nostro repository (TitleRespository.kt
). Definisci una nuova funzione che utilizzi l'operatore suspend
per comunicare a Kotlin che funziona con le coroutine.
TitoloRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Al termine di questo codelab, lo aggiorneremo in modo da utilizzare Retrofit e Room per recuperare un nuovo titolo e scriverlo nel database utilizzando coroutine. Per il momento, spenderà 500 millisecondi fingendosi di lavorare e poi di continuare.
In MainViewModel
, sostituisci la versione di callback di refreshTitle
con una che lancia una nuova coroutine:
MainViewModel.kt
/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Analizziamo questa funzione:
viewModelScope.launch {
Proprio come la coroutine per aggiornare il conteggio dei tocchi, inizia lanciando una nuova coroutine in viewModelScope
. Verrà utilizzato Dispatchers.Main
, che è consentito. Anche se refreshTitle
effettuerà una richiesta di rete e una query del database, potrà utilizzare le coroutine per esporre un'interfaccia main-safe. Ciò significa che sarà sicuro chiamarlo dal thread principale.
Poiché stiamo utilizzando viewModelScope
, quando l'utente si allontana dalla schermata, il lavoro iniziato da questa corona verrà annullato automaticamente. Ciò significa che non effettuerà richieste di rete aggiuntive o query di database.
Nelle prossime righe di codice viene effettivamente chiamata refreshTitle
nella repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Prima di eseguire questa operazione, la rotellina di caricamento avvia la rotellina di caricamento, che chiama refreshTitle
come una normale funzione. Tuttavia, poiché refreshTitle
è una funzione di sospensione, viene eseguita in modo diverso rispetto a una funzione normale.
Non dobbiamo superare un callback. La coroutine verrà sospesa fino a quando non verrà ripristinata entro il giorno refreshTitle
. Anche se assomiglia a una normale chiamata della funzione di blocco, attendi automaticamente che la query sulla rete e sul database sia completata prima di riprendere senza bloccare il thread principale.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Le eccezioni nelle funzioni di sospensione funzionano come gli errori nelle funzioni normali. Se generi un errore in una funzione di sospensione, l'errore verrà trasmesso al chiamante. Pertanto, anche se il loro funzionamento è molto diverso, puoi gestirli con dei blocchi standard. È un'opzione utile perché ti permette di affidarti al supporto delle lingue integrato per la gestione degli errori invece che alla creazione di una gestione personalizzata degli errori per ogni callback.
Inoltre, se pubblichi un'eccezione fuori da una coroutine, per impostazione predefinita la coroutine la annullerà. Ciò significa che è facile annullare diverse attività correlate.
Infine, possiamo fare in modo che la rotellina funzioni sempre quando la query è in esecuzione.
Esegui di nuovo l'applicazione selezionando la configurazione start e poi premendo . Quando tocchi un punto qualsiasi, dovresti vedere una rotellina di caricamento. Il titolo non cambierà, perché non abbiamo ancora collegato la nostra rete o il nostro database.
Nel prossimo esercizio aggiornerai il repository in modo che funzioni effettivamente.
In questo esercizio imparerai a impostare il thread su cui viene eseguita una coroutine per implementare una versione funzionante di TitleRepository
.
Esaminare il codice di callback esistente in refreshTitle
Apri TitleRepository.kt
e rivedi l'implementazione basata su callback esistente.
TitoloRepository.kt
// TitleRepository.kt
fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
// This request will be run on a background thread by retrofit
BACKGROUND.submit {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle().execute()
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
// Inform the caller the refresh is completed
titleRefreshCallback.onCompleted()
} else {
// If it's not successful, inform the callback of the error
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", null))
}
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
titleRefreshCallback.onError(
TitleRefreshError("Unable to refresh title", cause))
}
}
}
In TitleRepository.kt
, il metodo refreshTitleWithCallbacks
è implementato con un callback per comunicare lo stato del caricamento e dell'errore al chiamante.
Questa funzione esegue diverse operazioni per implementare l'aggiornamento,
- Passa a un altro thread con
BACKGROUND
ExecutorService
- Esegui la richiesta di rete
fetchNextTitle
utilizzando il metodoexecute()
di blocco. In questo modo verrà eseguita la richiesta di rete nel thread corrente, in questo caso uno dei thread inBACKGROUND
. - Se il risultato è positivo, salvalo nel database con
insertTitle
e chiama il metodoonCompleted()
. - Se il risultato non è riuscito o c'è un'eccezione, chiama il metodo onError per informare il chiamante dell'aggiornamento non riuscito.
Questa implementazione basata sul callback è main-safe perché non bloccherà il thread principale. Tuttavia, deve utilizzare un callback per informare il chiamante quando il lavoro è stato completato. Chiama anche i callback nel thread BACKGROUND
che è cambiato.
Chiamate di blocco delle chiamate da coroutine
Senza introdurre coroutine nella rete o nel database, possiamo rendere questo codice main-safe utilizzando coroutine. In questo modo potremo eliminare il callback e consentirci di ritrasmettere il risultato al thread che lo ha inizialmente chiamato.
Puoi utilizzare questo pattern ogni volta che devi svolgere un blocco o un lavoro intensivo della CPU dall'interno di una coroutine, come ordinare e filtrare un vasto elenco o leggere dal disco.
Per passare da un supervisore all'altro, le coroutine utilizzano withContext
. Chiamata a withContext
che passa all'altro supervisore solo per il lambda per poi tornare al supervisore che lo ha chiamato con il risultato della lambda.
Per impostazione predefinita, le coroutine Kotlin forniscono tre corrieri: Main
, IO
e Default
. Il supervisore IO è ottimizzato per il lavoro IO, ad esempio la lettura dalla rete o dal disco, mentre il supervisore predefinito è ottimizzato per le attività che richiedono un uso intensivo della CPU.
TitoloRepository.kt
suspend fun refreshTitle() {
// interact with *blocking* network and IO calls from a coroutine
withContext(Dispatchers.IO) {
val result = try {
// Make network request using a blocking call
network.fetchNextTitle().execute()
} catch (cause: Throwable) {
// If the network throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
if (result.isSuccessful) {
// Save it to database
titleDao.insertTitle(Title(result.body()!!))
} else {
// If it's not successful, inform the callback of the error
throw TitleRefreshError("Unable to refresh title", null)
}
}
}
Questa implementazione utilizza chiamate di blocco per la rete e il database, ma è ancora leggermente più semplice rispetto alla versione di callback.
Questo codice utilizza ancora le chiamate blocco. Le chiamate a execute()
e insertTitle(...)
bloccheranno entrambi il thread in cui è in esecuzione questa corona. Tuttavia, passando a Dispatchers.IO
utilizzando withContext
, bloccheremo uno dei thread nel supervisore dell'ordine di inserzione. La coroutine che l'ha chiamata, potenzialmente in esecuzione su Dispatchers.Main
, sarà sospesa fino al completamento del lambda withContext
.
Rispetto alla versione di callback, esistono due differenze importanti:
withContext
restituisce il risultato al mittente che lo ha chiamato, in questo casoDispatchers.Main
. La versione di callback denominata callback in un thread nel servizio esecutoreBACKGROUND
.- Il chiamante non deve trasferire un callback a questa funzione. Possono fare affidamento sulla sospensione e sul ripristino per ottenere il risultato o l'errore.
Esegui di nuovo l'app
Se esegui di nuovo l'app, noterai che la nuova implementazione basata su Coroutines carica i risultati dalla rete.
Nel prossimo passaggio integrerai le coroutine in Room e Retrofit.
Per continuare l'integrazione delle coroutine, utilizzeremo il supporto per la sospensione delle funzioni nella versione stabile di Room and Retrofit e semplificheremo quindi il codice appena scritto utilizzando le funzioni di sospensione.
Coroutine in camera
Per prima cosa, apri MainDatabase.kt
e imposta insertTitle
come funzione di sospensione:
Database principale.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
In questo modo, la stanza virtuale renderà la tua query main-safe ed verrà eseguita automaticamente su un thread in background. Tuttavia, ciò significa che puoi chiamare questa query solo dall'interno di una coroutine.
E questo è tutto ciò che devi fare per utilizzare le coroutine nella stanza. Grazioso.
Coroutine in retrofit
Ora vediamo come integrare le coroutine con il Retrofit. Apri MainNetwork.kt
e cambia fetchNextTitle
in una funzione di sospensione.
MainNetwork.kt
// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String
interface MainNetwork {
@GET("next_title.json")
suspend fun fetchNextTitle(): String
}
Per utilizzare le funzioni di sospensione con Retrofit, devi eseguire due operazioni:
- Aggiungi un modificatore di sospensione alla funzione
- Rimuovi il wrapper
Call
dal tipo di ritorno. Qui stiamo tornando all'appString
, ma potresti restituire anche un tipo complesso basato su json. Se vuoi comunque fornire l'accesso alla versione completa diResult
, puoi restituireResult<String>
anzichéString
dalla funzione di sospensione.
Retrofit attiverà automaticamente le funzioni main-safe in modo che tu possa chiamarle direttamente da Dispatchers.Main
.
Utilizzo di Room e Retrofit
Ora che le funzionalità Room and Retrofit supportano la sospensione, possiamo utilizzarle dal nostro repository. Apri TitleRepository.kt
e scopri come l'utilizzo delle funzioni di sospensione semplifica notevolmente la logica, anche rispetto alla versione di blocco:
TitoloRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Wow, è molto più breve. Che cosa è successo? In definitiva, la necessità di sospendere e riprendere la pubblicazione fa sì che il codice sia molto più breve. Retrofit ci permette di utilizzare qui tipi di resi come l'oggetto String
o un oggetto User
, anziché un elemento Call
. Questo è sicuro, perché all'interno della funzione di sospensione, Retrofit
è in grado di eseguire la richiesta di rete su un thread in background e riprendere la coroutine al termine della chiamata.
Meglio ancora, ci siamo sbarazzati di withContext
. Poiché sia la camera sia il retrofit forniscono le funzioni di sospensione principale-sicuro, è sicuro orchestrare questo lavoro asincrono da Dispatchers.Main
.
Correggere gli errori del compilatore
Il passaggio alle coroutine comporta la modifica della firma delle funzioni, in quanto non è possibile chiamare una funzione di sospensione da una funzione normale. Quando hai aggiunto il modificatore suspend
in questo passaggio, sono stati generati alcuni errori di compilazione che mostrano cosa accade se viene modificata una funzione da sospendere in un progetto reale.
Rivedi il progetto e correggi gli errori di compilazione modificando la funzione per sospendere la creazione. Ecco le risoluzioni rapide per ciascuna di esse:
TestingFakes.kt
Aggiorna i falsi test per supportare i nuovi modificatori di sospensione.
TitoloDaoFake
- Premi Alt-Invio e modificatori di sospensione per tutte le funzioni nell'area gerarchica
MainNetworkFake
- Premi Alt-Invio e modificatori di sospensione per tutte le funzioni nell'area gerarchica
- Sostituisci
fetchNextTitle
con questa funzione
override suspend fun fetchNextTitle() = result
MainNetworkpuòbleFake
- Premi Alt-Invio e modificatori di sospensione per tutte le funzioni nell'area gerarchica
- Sostituisci
fetchNextTitle
con questa funzione
override suspend fun fetchNextTitle() = completable.await()
TitoloRepository.kt
- Elimina la funzione
refreshTitleWithCallbacks
perché non viene più utilizzata.
Esegui l'app
Esegui di nuovo l'app, dopo averla compilata, vedrai che carica i dati utilizzando le coroutine da Viewmodel a Room and Retrofit!
Congratulazioni, hai sostituito completamente questa app con l'uso di coroutine. Per concludere, parleremo un po' di come testare ciò che abbiamo appena fatto.
In questo esercizio scriverai un test che chiama direttamente una funzione suspend
.
Poiché refreshTitle
viene esposto come API pubblica, verrà testato direttamente, mostrando come chiamare le funzioni delle coroutine dai test.
Ecco la funzione refreshTitle
che hai implementato nell'ultimo esercizio:
TitoloRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = network.fetchNextTitle()
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Scrivere un test che chiami una funzione di sospensione
Apri TitleRepositoryTest.kt
nella cartella test
, che contiene due TODO.
Prova a chiamare refreshTitle
dal primo test whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Poiché refreshTitle
è una funzione suspend
, Kotlin non sa come chiamarla, tranne che da una funzione coroutine o un'altra funzione di sospensione, quindi riceverai un errore di compilazione, ad esempio "Suspend function refreshTitle dovrebbe essere chiamato solo da una coroutine o da un'altra funzione di sospensione.
Il runner non sa nulla delle coroutine, pertanto non possiamo rendere questo test una funzione di sospensione. Potremmo assegnare una coroutina a launch
utilizzando un CoroutineScope
come in un ViewModel
, ma i test devono portare a termine le coroutine prima di tornare. Una volta restituita una funzione di test, il test è terminato. Le coroutine avviate con launch
sono codice asincrono, che potrebbe essere completato in futuro. Di conseguenza, per testare il codice asincrono, hai bisogno di un modo per indicare al test di attendere fino al completamento della coroutine. Dato che launch
è una chiamata non di blocco, torna subito e può continuare a eseguire una coroutine al termine della funzione. Non può quindi essere utilizzata nei test. Ad esempio:
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
// launch starts a coroutine then immediately returns
GlobalScope.launch {
// since this is asynchronous code, this may be called *after* the test completes
subject.refreshTitle()
}
// test function returns immediately, and
// doesn't see the results of refreshTitle
}
Questo test a volte avrà esito negativo. La chiamata al numero launch
tornerà immediatamente ed verrà eseguita contemporaneamente al resto dello scenario di test. Il test non è in grado di sapere se refreshTitle
è stato ancora eseguito o meno e che eventuali dichiarazioni come il controllo dell'aggiornamento del database sarebbero errate. Inoltre, se refreshTitle
ha generato un'eccezione, questa non verrà generata nello stack di chiamate di prova. Verrà invece trasferito al gestore di eccezioni non rilevato di GlobalScope
.
La libreria kotlinx-coroutines-test
ha la funzione runBlockingTest
che si blocca mentre le chiamate vengono sospese. Quando runBlockingTest
chiama una funzione di sospensione o launches
una nuova coroutine, questa viene eseguita per impostazione predefinita. Puoi definirlo come un modo per convertire le funzioni di sospensione e le coroutine in chiamate di funzione normali.
Inoltre, runBlockingTest
ripristinerà le eccezioni non rilevate per te. In questo modo è più semplice verificare quando una coroutine genera un'eccezione.
Implementare un test con una sola corona
Aggrega la chiamata a refreshTitle
con runBlockingTest
e rimuovi il wrapper GlobalScope.launch
da subject.refreshTitle().
TitoloRepositoryTest.kt
@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
val titleDao = TitleDaoFake("title")
val subject = TitleRepository(
MainNetworkFake("OK"),
titleDao
)
subject.refreshTitle()
Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}
Questo test utilizza i falsi indicati per verificare che"OK"venga inserito nel database da refreshTitle
.
Quando il test chiama runBlockingTest
, si bloccherà fino al completamento della coroutina di runBlockingTest
. Poi, quando chiamiamo refreshTitle
, utilizza il normale meccanismo di sospensione e ripristino per attendere l'aggiunta della riga di database al nostro falso.
Una volta completata la coroutine del test, runBlockingTest
torna.
Scrivere un test di timeout
Vogliamo aggiungere un breve timeout alla richiesta di rete. Scriviamo prima il test e poi implementiamo il timeout. Crea un nuovo test:
TitoloRepositoryTest.kt
@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
val network = MainNetworkCompletableFake()
val subject = TitleRepository(
network,
TitleDaoFake("title")
)
launch {
subject.refreshTitle()
}
advanceTimeBy(5_000)
}
Questo test utilizza il falso MainNetworkCompletableFake
fornito, che è un falso di rete progettato per sospendere i chiamanti fino a quando non viene continuato. Quando refreshTitle
tenta di effettuare una richiesta di rete, si blocca per sempre perché vogliamo testare i timeout.
Quindi avvia una coroutine separata per chiamare refreshTitle
. Questa è una parte fondamentale del timeout dei test: il timeout dovrebbe avvenire in una finestra diversa da quella creata da runBlockingTest
. In questo modo possiamo chiamare la riga successiva, advanceTimeBy(5_000)
, che avanzerà di cinque secondi e causerà il timeout dell'altra coroutine.
Questo è un test di timeout completo e verrà superato una volta implementato il timeout.
Eseguilo ora per capire cosa succede:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Una delle caratteristiche di runBlockingTest
è che non ti consente di fumare coroutine al termine del test. La presenza di coroutine non completate, come la nostra coroutine di lancio, al termine del test avrà esito negativo.
Aggiungere un timeout
Apri TitleRepository
e aggiungi un timeout di cinque secondi al recupero della rete. A tale scopo, utilizza la funzione withTimeout
:
TitoloRepository.kt
suspend fun refreshTitle() {
try {
// Make network request using a blocking call
val result = withTimeout(5_000) {
network.fetchNextTitle()
}
titleDao.insertTitle(Title(result))
} catch (cause: Throwable) {
// If anything throws an exception, inform the caller
throw TitleRefreshError("Unable to refresh title", cause)
}
}
Esegui il test. Quando esegui i test, dovresti vedere che vengono superati tutti gli altri.
Nel prossimo esercizio imparerai a scrivere funzioni di ordine superiore utilizzando coroutine.
In questo esercizio dovrai eseguire il refactoring di refreshTitle
in MainViewModel
per utilizzare una funzione di caricamento generale. Questo ti insegnerà come creare funzioni di ordine più elevato che utilizzano coroutine.
L'implementazione attuale di refreshTitle
funziona, ma possiamo creare una coroutine di caricamento generale dei dati che mostra sempre la rotellina. Ciò può essere utile in un codebase che carica i dati in risposta a diversi eventi e vuole assicurarsi che la rotellina di caricamento venga visualizzata in modo coerente.
La verifica dell'implementazione corrente su ogni riga tranne repository.refreshTitle()
è un testo boilerplate per mostrare gli errori di rotazione e visualizzazione.
// MainViewModel.kt
fun refreshTitle() {
viewModelScope.launch {
try {
_spinner.value = true
// this is the only part that changes between sources
repository.refreshTitle()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Utilizzo di coroutine nelle funzioni di ordine superiore
Aggiungi questo codice a MainViewmodel.kt
MainViewModel.kt
private fun launchDataLoad(block: suspend () -> Unit): Job {
return viewModelScope.launch {
try {
_spinner.value = true
block()
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
}
}
Ora refactoring refreshTitle()
per utilizzare questa funzione di ordine superiore.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Astraendo la logica intorno alla visualizzazione di una rotellina di caricamento e alla visualizzazione di errori, abbiamo semplificato il nostro codice effettivo necessario per caricare i dati. Mostrare una rotellina o visualizzare un errore è un aspetto che può essere semplificato facilmente durante il caricamento dei dati, mentre l'origine dati e la destinazione effettive devono essere specificate ogni volta.
Per creare questa astrazione, launchDataLoad
richiede un argomento block
che è una sospensione lambda. La sospensione di un lambda ti consente di chiamare le funzioni di sospensione. Ecco come Kotlin ha implementato i builder di coroutine launch
e runBlocking
che abbiamo utilizzato in questo codelab.
// suspend lambda
block: suspend () -> Unit
Per sospendere un lambda, inizia con la parola chiave suspend
. La freccia funzione e il tipo di ritorno Unit
completano la dichiarazione.
Spesso non devi dichiarare i tuoi lambda sospesi, ma possono essere utili per creare astrazioni come questa che incapsulano la logica ripetuta.
In questo esercizio imparerai a utilizzare il codice basato su coroutine di WorkManager.
Cos'è WorkManager
Su Android sono disponibili molte opzioni per il lavoro in background, che è consigliabile. Questo esercizio illustra come integrare WorkManager con le coroutine. WorkManager è una libreria compatibile, semplice e flessibile per lavorare in background in modo semplice. WorkManager è la soluzione consigliata per questi casi d'uso su Android.
WorkManager fa parte di Android Jetpack e un componente di architettura per le operazioni in background che richiedono una combinazione di esecuzione opportunitàstica e garantita. Con l'esecuzione agevole, WorkManager esegue il lavoro in background il prima possibile. Con l'esecuzione garantita, WorkManager si occupa della logica per avviare il lavoro in una serie di situazioni, anche se esci dalla tua app.
Per questo motivo, WorkManager è un'ottima scelta per le attività che devono essere completate alla fine.
Di seguito sono riportati alcuni esempi di attività che sono un buon utilizzo di WorkManager:
- Caricamento dei log in corso...
- Applicare filtri alle immagini e salvarle
- Sincronizzare periodicamente i dati locali con la rete
Utilizzo di coroutine con WorkManager
WorkManager fornisce implementazioni diverse della classe base ListanableWorker
per diversi casi d'uso.
La classe Worker più semplice ci consente di eseguire alcune operazioni sincrone da parte di WorkManager. Tuttavia, finora abbiamo lavorato per convertire il nostro codebase in modo da utilizzare le coroutine e sospendere le funzioni, il modo migliore per utilizzare WorkManager è tramite la classe CoroutineWorker
, che consente di definire la funzione doWork()
come funzione di sospensione.
Per iniziare, apri RefreshMainDataWork
. Si estende già in CoroutineWorker
e devi implementare doWork
.
All'interno della funzione suspend
doWork
, chiama refreshTitle()
dal repository e restituisce il risultato appropriato.
Una volta completato il TODO, il codice sarà simile a questo:
override suspend fun doWork(): Result {
val database = getDatabase(applicationContext)
val repository = TitleRepository(network, database.titleDao)
return try {
repository.refreshTitle()
Result.success()
} catch (error: TitleRefreshError) {
Result.failure()
}
}
CoroutineWorker.doWork()
è una funzione di sospensione. A differenza della classe Worker
più semplice, questo codice NON viene eseguito sull'esecutore specificato nella configurazione di WorkManager; tuttavia, utilizza il supervisore in qualità di membro di coroutineContext
(per impostazione predefinita, Dispatchers.Default
).
Test del nostro CoroutineWorker
Nessun codebase deve essere completo senza eseguire test.
WorkManager mette a disposizione un paio di modi per testare le classi di Worker
. Per saperne di più sull'infrastruttura di test originale, consulta la documentazione.
WorkManager v2.1 introduce un nuovo set di API per supportare un modo più semplice di testare le classi di ListenableWorker
e, di conseguenza, CoroutineWorker. Nel nostro codice utilizzeremo una di queste nuove API: TestListenableWorkerBuilder
.
Per aggiungere il nostro nuovo test, aggiorna il file RefreshMainDataWorkTest
nella cartella androidTest
.
I contenuti del file sono:
package com.example.android.kotlincoroutines.main
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {
@Test
fun testRefreshMainDataWork() {
val fakeNetwork = MainNetworkFake("OK")
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
.setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
.build()
// Start the work synchronously
val result = worker.startWork().get()
assertThat(result).isEqualTo(Result.success())
}
}
Prima di effettuare il test, comunichiamo a WorkManager
la fabbrica in modo che possiamo iniettare la rete falsa.
Il test stesso utilizza l'TestListenableWorkerBuilder
per creare il worker, dopodiché è possibile chiamare il metodo startWork()
.
WorkManager è solo un esempio di come è possibile utilizzare le coroutine per semplificare la progettazione delle API.
In questo codelab abbiamo esaminato i concetti di base di cui hai bisogno per iniziare a utilizzare le coroutine nella tua app.
Abbiamo trattato i seguenti argomenti:
- Come integrare le attività coroutine nelle app Android sia dall'interfaccia utente che dai job WorkManager per semplificare la programmazione asincrona,
- Come utilizzare coroutine all'interno di un
ViewModel
per recuperare i dati dalla rete e salvarli in un database senza bloccare il thread principale. - E come annullare tutte le coroutine al termine di
ViewModel
.
Per testare il codice basato su coroutine, abbiamo trattato entrambi i test e abbiamo chiamato direttamente le funzioni suspend
dai test.
Scopri di più
Controlla il codelab;Advanced Coroutines with Kotlin Flow and LiveData" per scoprire un uso più avanzato delle coroutine su Android.
Le coroutine Kotlin presentano molte funzionalità che non sono state coperte da questo codelab. Se vuoi saperne di più sulle coroutine Kotlin, leggi le guide alle coroutine pubblicate da JetBrains. Dai un'occhiata anche a "Migliora le prestazioni dell'app con le coroutine Kotlin" per ottenere ulteriori schemi di utilizzo delle coroutine su Android.