In questo codelab imparerai a utilizzare le coroutine Kotlin in un'app per Android, un nuovo modo di gestire i thread in background che può semplificare il codice riducendo la necessità di callback. Le coroutine sono una funzionalità di Kotlin che converte i callback asincroni per le attività di lunga durata, come l'accesso al database o alla rete, in codice sequenziale.
Ecco uno snippet di codice per darti un'idea di cosa dovrai fare.
// 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 i componenti dell'architettura, che utilizza uno stile di callback per le attività di lunga durata.
Al termine di questo codelab, avrai acquisito esperienza sufficiente per utilizzare le coroutine nella tua app per caricare dati dalla rete e potrai integrarle in un'app. Inoltre, conoscerai le best practice per le coroutine e come scrivere un test per il codice che le utilizza.
Prerequisiti
- Familiarità con i componenti dell'architettura
ViewModel
,LiveData
,Repository
eRoom
. - Esperienza con la sintassi Kotlin, incluse le funzioni di estensione e le espressioni lambda.
- Una conoscenza di base dell'utilizzo dei thread su Android, inclusi il thread principale, i thread in background e i callback.
Attività previste
- Chiama il codice scritto con le coroutine e ottieni i risultati.
- Utilizza le funzioni di sospensione per rendere sequenziale il codice asincrono.
- Utilizza
launch
erunBlocking
per controllare l'esecuzione del codice. - Scopri le tecniche per convertire le API esistenti in coroutine utilizzando
suspendCoroutine
. - Utilizza le coroutine con i componenti dell'architettura.
- Scopri le best practice per testare le coroutine.
Che cosa ti serve
- Android Studio 3.5 (il codelab potrebbe funzionare con altre versioni, ma alcune funzionalità potrebbero non essere disponibili o avere un aspetto diverso).
Se riscontri problemi (bug del codice, errori grammaticali, formulazione poco chiara e così via) mentre segui questo codelab, segnalali 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 per questo codelab:
... o 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 l'aspetto dell'app di esempio iniziale. Segui queste istruzioni per aprire l'app di esempio in Android Studio.
- Se hai scaricato il file ZIP
kotlin-coroutines
, decomprimilo. - Apri il progetto
coroutines-codelab
in Android Studio. - Seleziona il modulo dell'applicazione
start
. - Fai clic sul pulsante
Esegui e scegli un emulatore o collega il tuo dispositivo Android, che deve essere in grado di eseguire Android Lollipop (l'SDK minimo supportato è 21). Dovrebbe essere visualizzata la schermata Kotlin Coroutines:
Questa app iniziale utilizza i thread per incrementare il conteggio con un breve ritardo dopo aver premuto lo schermo. Verrà recuperato anche un nuovo titolo dalla rete e visualizzato sullo schermo. Prova subito e dovresti vedere il conteggio e il messaggio cambiare dopo un breve ritardo. In questo codelab convertirai questa applicazione per utilizzare le coroutine.
Questa app utilizza i componenti dell'architettura per separare il codice dell'interfaccia utente in MainActivity
dalla logica dell'applicazione in MainViewModel
. Prenditi un momento per familiarizzare con la struttura del progetto.
MainActivity
mostra la UI, registra i listener di clic e può visualizzare unSnackbar
. Trasmette gli eventi aMainViewModel
e aggiorna lo schermo in base aLiveData
inMainViewModel
.MainViewModel
gestisce gli eventi inonMainViewClicked
e comunicherà conMainActivity
utilizzandoLiveData.
Executors
definisceBACKGROUND,
, che può eseguire operazioni 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 tuo progetto. I progetti del codelab sono già stati creati, quindi non devi farlo per completare il codelab.
Le coroutine su Android sono disponibili come libreria principale ed estensioni specifiche per Android:
- kotlinx-coroutines-core : interfaccia principale per l'utilizzo delle coroutine in Kotlin
- kotlinx-coroutines-android : supporto del thread principale di Android nelle 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 delle 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 alla UI. È anche il thread che chiama tutti i gestori di clic e altri callback dell'interfaccia utente. Pertanto, deve funzionare senza problemi per garantire un'esperienza utente ottimale.
Affinché la tua app venga visualizzata dall'utente 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, chiamare codice come questo dal thread principale può causare la pausa, l'interruzione o persino il blocco dell'app. Se blocchi il thread principale per troppo tempo, l'app potrebbe anche arrestarsi in modo anomalo e mostrare una finestra di dialogo L'applicazione non risponde.
Guarda il video di seguito per scoprire in che modo le coroutine risolvono questo problema su Android introducendo la sicurezza del thread principale.
Il pattern di richiamata
Un pattern per eseguire attività di lunga durata senza bloccare il thread principale è quello dei callback. Utilizzando i callback, puoi avviare attività di lunga durata su un thread in background. Al termine dell'attività, viene chiamata la funzione di callback per comunicarti il risultato sul thread principale.
Dai un'occhiata a un esempio del pattern di 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 eseguito abbastanza velocemente da essere eseguito sul thread principale. Ciò significa che deve tornare molto rapidamente, in modo che l'aggiornamento successivo dello schermo non venga ritardato. Tuttavia, poiché il completamento di slowFetch
richiede secondi o addirittura minuti, 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 callback sono un ottimo pattern, ma presentano alcuni svantaggi. Il codice che utilizza molto i callback può diventare difficile da leggere e da comprendere. Inoltre, i callback non consentono l'utilizzo di alcune funzionalità del linguaggio, come le eccezioni.
Le coroutine Kotlin ti consentono di convertire il codice basato su callback in codice sequenziale. Il codice scritto in sequenza è in genere più facile da leggere e può persino utilizzare funzionalità del linguaggio come le eccezioni.
Alla fine, fanno esattamente la stessa cosa: attendono che un risultato sia disponibile da un'attività a esecuzione prolungata e continuano l'esecuzione. Tuttavia, nel codice hanno un aspetto molto diverso.
La parola chiave suspend
è il modo in cui Kotlin contrassegna una funzione o un tipo di funzione disponibile per le coroutine. Quando una coroutine chiama una funzione contrassegnata con suspend
, anziché bloccarsi finché la funzione non restituisce un valore come una normale chiamata di funzione, sospende l'esecuzione finché il risultato non è pronto, quindi riprende l'esecuzione da dove si era interrotta con il risultato. Mentre è sospesa in attesa di un risultato, sblocca il thread su cui è in esecuzione in modo che possano essere eseguite altre funzioni o 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 per la versione di callback, makeNetworkRequest
deve essere restituito immediatamente dal thread principale perché è contrassegnato come @UiThread
. Ciò significa che in genere non è possibile chiamare metodi di blocco come slowFetch
. È qui che la parola chiave suspend
fa la sua magia.
Rispetto al codice basato su callback, il codice delle coroutine ottiene lo stesso risultato di sblocco del thread corrente con meno codice. Grazie al suo stile sequenziale, è facile concatenare diverse attività a esecuzione prolungata senza 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 nelle 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 introdurrai le coroutine nell'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 vengono eseguite all'interno di un CoroutineScope
. Un ambito controlla la durata delle coroutine tramite il relativo job. Quando annulli il job di un ambito, vengono annullate tutte le coroutine avviate in quell'ambito. Su Android, puoi utilizzare un ambito per annullare tutte le coroutine in esecuzione quando, ad esempio, l'utente esce da un Activity
o da un Fragment
. Gli ambiti consentono anche di specificare un dispatcher predefinito. Un dispatcher controlla quale thread esegue una coroutine.
Per le coroutine avviate dalla UI, in genere è corretto avviarle su Dispatchers.Main
, ovvero il thread principale su Android. Una coroutine avviata su Dispatchers.Main
non bloccherà il thread principale durante la sospensione. Poiché una coroutine ViewModel
aggiorna quasi sempre la UI sul thread principale, l'avvio di coroutine sul thread principale consente di risparmiare cambi di thread aggiuntivi. Una coroutine avviata sul thread principale può cambiare dispatcher in qualsiasi momento dopo l'avvio. Ad esempio, può utilizzare un altro dispatcher per analizzare un risultato JSON di grandi dimensioni dal thread principale.
Utilizzare viewModelScope
La libreria AndroidX lifecycle-viewmodel-ktx
aggiunge un CoroutineScope ai ViewModel configurato per avviare le coroutine correlate all'interfaccia utente. Per utilizzare questa libreria, devi includerla nel file build.gradle (Module: start)
del tuo progetto. Questo passaggio è già stato eseguito nei progetti del codelab.
dependencies { ... implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x" }
La libreria aggiunge un viewModelScope
come funzione di estensione della classe ViewModel
. Questo ambito è associato a Dispatchers.Main
e verrà annullato automaticamente quando ViewModel
viene cancellato.
Passare dai thread alle coroutine
In MainViewModel.kt
trova il successivo 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 BACKGROUND ExecutorService
(definito in util/Executor.kt
) per essere eseguito in un thread in background. Poiché sleep
blocca il thread corrente, l'UI si bloccherebbe se venisse chiamata sul thread principale. Un secondo dopo che l'utente ha fatto clic sulla visualizzazione principale, viene richiesta una snackbar.
Puoi vedere cosa succede rimuovendo BACKGROUND dal codice ed eseguendolo di nuovo. L'indicatore di caricamento non verrà visualizzato e tutto "salterà" 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 fa la stessa cosa. 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 fa la stessa cosa, aspettando un secondo prima di mostrare una snackbar. Tuttavia, esistono alcune differenze importanti:
viewModelScope.
launch
avvierà una coroutine inviewModelScope
. Ciò significa che quando il job che abbiamo passato aviewModelScope
viene annullato, tutte le coroutine in questo job/ambito verranno annullate. Se l'utente ha abbandonato l'attività prima chedelay
restituisse un valore, questa coroutine verrà annullata automaticamente quando viene chiamatoonCleared
al momento dell'eliminazione di ViewModel.- Poiché
viewModelScope
ha un dispatcher predefinito diDispatchers.Main
, questa coroutine verrà avviata nel thread principale. Vedremo più avanti come utilizzare thread diversi. - La funzione
delay
è una funzionesuspend
. In Android Studio, questa informazione viene mostrata dall'iconanel margine sinistro. Anche se questa coroutine viene eseguita sul thread principale,
delay
non bloccherà il thread per un secondo. Il dispatcher pianificherà la ripresa della coroutine tra un secondo alla prossima istruzione.
Esegui la query. Quando fai clic sulla visualizzazione principale, dopo un secondo dovresti visualizzare una snackbar.
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. Per consentirci di testare MainViewModel in un test off-device, vengono utilizzate due regole:
InstantTaskExecutorRule
è una regola JUnit che configuraLiveData
per eseguire ogni attività in modo sincronoMainCoroutineScopeRule
è una regola personalizzata in questo codebase che configuraDispatchers.Main
per utilizzare unTestCoroutineDispatcher
dakotlinx-coroutines-test
. Ciò consente ai test di far avanzare un orologio virtuale per i test e al codice di utilizzareDispatchers.Main
nei test delle unità.
Nel metodo setup
, viene creata una nuova istanza di MainViewModel
utilizzando test fittizi, ovvero implementazioni fittizie della rete e del database fornite nel codice iniziale per scrivere test senza utilizzare la rete o il database reali.
Per questo test, i fakes sono necessari solo per soddisfare le dipendenze di MainViewModel
. Più avanti in questo codelab aggiornerai i test fittizi per supportare le coroutine.
Scrivere un test che controlla le coroutine
Aggiungi un nuovo test che assicuri che i tocchi vengano aggiornati un secondo dopo aver fatto 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")
}
Chiamando onMainViewClicked
, verrà avviata la coroutine che abbiamo appena creato. Questo test verifica che il testo dei tocchi rimanga "0 tocchi" subito dopo la chiamata di onMainViewClicked
, poi 1 secondo dopo venga aggiornato a "1 tocco".
Questo test utilizza virtual-time per controllare l'esecuzione della coroutine avviata da onMainViewClicked
. MainCoroutineScopeRule
ti consente di mettere in pausa, riprendere o controllare l'esecuzione delle coroutine avviate su Dispatchers.Main
. Qui chiamiamo advanceTimeBy(1_000)
, che farà in modo che il dispatcher principale esegua immediatamente le coroutine pianificate per riprendere l'esecuzione 1 secondo dopo.
Questo test è completamente deterministico, il che significa che verrà sempre eseguito nello stesso modo. Inoltre, poiché ha il controllo completo dell'esecuzione delle coroutine avviate su Dispatchers.Main
, non deve attendere un secondo per l'impostazione del valore.
Eseguire il test esistente
- Fai clic con il tasto destro del mouse sul nome della classe
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
nella barra degli strumenti. Per impostazione predefinita, la configurazione verrà chiamata MainViewModelTest.
Dovresti vedere che il test è stato superato. L'operazione dovrebbe richiedere molto meno di un secondo.
Nel prossimo esercizio imparerai a eseguire la conversione dalle API di callback esistenti per utilizzare le 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
.
È consigliabile capire di cosa è responsabile ogni parte dell'architettura prima di passare all'utilizzo delle coroutine.
MainDatabase
implementa un database utilizzando Room che salva e carica unTitle
.MainNetwork
implementa un'API di rete che recupera un nuovo titolo. Utilizza Retrofit per recuperare i titoli.Retrofit
è configurato per restituire errori o dati simulati in modo casuale, ma altrimenti si comporta come se stesse effettuando 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. Indica al repository di aggiornare il titolo quando l'utente tocca lo schermo.
Poiché la richiesta di rete è guidata dagli eventi dell'interfaccia utente e vogliamo avviare una coroutine in base a questi eventi, il posto naturale per iniziare a utilizzare le coroutine è in ViewModel
.
La versione di richiamata
Apri MainViewModel.kt
per visualizzare 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 lo scriva nel database.
Questa implementazione utilizza un callback per eseguire alcune operazioni:
- Prima di iniziare una query, viene visualizzata una rotellina di caricamento con
_spinner.value = true
- Quando riceve un risultato, cancella l'indicatore di caricamento con
_spinner.value = false
- Se si verifica un errore, viene visualizzata una snackbar e viene cancellato lo spinner
Tieni presente che il callback onCompleted
non riceve 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 esattamente lo stesso comportamento. È una buona pratica utilizzare un'origine dati osservabile come un database Room
per mantenere automaticamente aggiornata la UI.
La versione delle coroutine
Riscriviamo refreshTitle
con le coroutine.
Poiché ci servirà subito, creiamo una funzione di sospensione vuota nel nostro repository (TitleRespository.kt
). Definisci una nuova funzione che utilizza l'operatore suspend
per indicare a Kotlin che funziona con le coroutine.
TitleRepository.kt
suspend fun refreshTitle() {
// TODO: Refresh from network and write to database
delay(500)
}
Al termine di questo codelab, aggiornerai questo codice per utilizzare Retrofit e Room per recuperare un nuovo titolo e scriverlo nel database utilizzando le coroutine. Per ora, impiegherà 500 millisecondi per simulare l'esecuzione di un'attività e poi continuerà.
In MainViewModel
, sostituisci la versione di callback di refreshTitle
con una che avvia 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 {
Come per la coroutine per aggiornare il conteggio dei tocchi, inizia avviando una nuova coroutine in viewModelScope
. Verrà utilizzato Dispatchers.Main
, il che va bene. Anche se refreshTitle
effettuerà una richiesta di rete e una query del database, può utilizzare le coroutine per esporre un'interfaccia sicura per il thread principale. Ciò significa che sarà sicuro chiamarlo dal thread principale.
Poiché utilizziamo viewModelScope
, quando l'utente si allontana da questa schermata, il lavoro iniziato da questa coroutine verrà annullato automaticamente. Ciò significa che non verranno effettuate richieste di rete o query di database aggiuntive.
Le righe di codice successive chiamano effettivamente refreshTitle
in repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Prima di fare qualsiasi cosa, questa coroutine avvia l'indicatore di caricamento, quindi chiama refreshTitle
come una normale funzione. Tuttavia, poiché refreshTitle
è una funzione di sospensione, viene eseguita in modo diverso rispetto a una funzione normale.
Non è necessario effettuare un callback. La coroutine verrà sospesa fino a quando non verrà ripresa da refreshTitle
. Sebbene sembri una normale chiamata di funzione di blocco, attenderà automaticamente il completamento della query di rete e del database 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 esattamente come gli errori nelle funzioni regolari. Se generi un errore in una funzione di sospensione, questo verrà generato per il chiamante. Quindi, anche se vengono eseguiti in modo molto diverso, puoi utilizzare i normali blocchi try/catch per gestirli. Ciò è utile perché ti consente di fare affidamento al supporto linguistico integrato per la gestione degli errori anziché creare una gestione degli errori personalizzata per ogni callback.
Inoltre, se generi un'eccezione da una coroutine, questa annullerà la coroutine principale per impostazione predefinita. Ciò significa che è facile annullare più attività correlate contemporaneamente.
Infine, in un blocco finally, possiamo assicurarci che lo spinner venga sempre disattivato dopo l'esecuzione della query.
Esegui di nuovo l'applicazione selezionando la configurazione start e premendo. Dovresti visualizzare un indicatore di caricamento quando tocchi un punto qualsiasi. Il titolo rimarrà invariato perché non abbiamo ancora collegato la nostra rete o il nostro database.
Nel prossimo esercizio aggiornerai il repository per eseguire effettivamente il lavoro.
In questo esercizio imparerai a cambiare il thread su cui viene eseguita una coroutine per implementare una versione funzionante di TitleRepository
.
Esamina il codice di callback esistente in refreshTitle
Apri TitleRepository.kt
e rivedi l'implementazione esistente basata su callback.
TitleRepository.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
viene implementato con un callback per comunicare lo stato di caricamento e di 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 metodo di bloccoexecute()
. In questo modo, la richiesta di rete verrà eseguita 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 è andato a buon fine o si è verificata un'eccezione, chiama il metodo onError per comunicare al chiamante che l'aggiornamento non è riuscito.
Questa implementazione basata su callback è sicura per il thread principale perché non lo blocca. Tuttavia, deve utilizzare un callback per informare il chiamante al termine dell'operazione. Chiama anche i callback sul thread BACKGROUND
a cui è passato.
Blocco delle chiamate dalle coroutine
Senza introdurre coroutine nella rete o nel database, possiamo rendere questo codice sicuro per il thread principale utilizzando le coroutine. In questo modo, potremo eliminare il callback e restituire il risultato al thread che lo ha chiamato inizialmente.
Puoi utilizzare questo pattern ogni volta che devi eseguire operazioni di blocco o che richiedono un utilizzo intensivo della CPU all'interno di una coroutine, ad esempio ordinare e filtrare un elenco di grandi dimensioni o leggere dal disco.
Per passare da un dispatcher all'altro, le coroutine utilizzano withContext
. La chiamata withContext
passa all'altro dispatcher solo per la lambda, poi torna al dispatcher che l'ha chiamata con il risultato della lambda.
Per impostazione predefinita, le coroutine Kotlin forniscono tre dispatcher: Main
, IO
e Default
. Il dispatcher I/O è ottimizzato per operazioni di I/O come la lettura dalla rete o dal disco, mentre il dispatcher predefinito è ottimizzato per le attività che richiedono un uso intensivo della CPU.
TitleRepository.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 è comunque un po' più semplice rispetto alla versione di callback.
Questo codice utilizza ancora le chiamate di blocco. Le chiamate a execute()
e insertTitle(...)
bloccheranno entrambe il thread in cui viene eseguita questa coroutine. Tuttavia, passando a Dispatchers.IO
utilizzando withContext
, blocchiamo uno dei thread nel dispatcher I/O. La coroutine che ha chiamato questa, possibilmente in esecuzione su Dispatchers.Main
, verrà sospesa fino al completamento della lambda withContext
.
Rispetto alla versione di callback, ci sono due differenze importanti:
withContext
restituisce il risultato al dispatcher che lo ha chiamato, in questo casoDispatchers.Main
. La versione di callback chiamava i callback su un thread nel servizio di esecuzioneBACKGROUND
.- Il chiamante non deve passare un callback a questa funzione. Possono fare affidamento alla sospensione e alla ripresa per ottenere il risultato o l'errore.
Esegui di nuovo l'app
Se esegui di nuovo l'app, vedrai che la nuova implementazione basata su coroutine carica i risultati dalla rete.
Nel passaggio successivo integrerai le coroutine in Room e Retrofit.
Per continuare l'integrazione delle coroutine, utilizzeremo il supporto per le funzioni di sospensione nella versione stabile di Room e Retrofit, quindi semplificheremo notevolmente il codice che abbiamo appena scritto utilizzando le funzioni di sospensione.
Coroutine in Room
Innanzitutto, apri MainDatabase.kt
e trasforma insertTitle
in una funzione di sospensione:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
In questo modo, Room renderà la tua query sicura per il thread principale e la eseguirà automaticamente su un thread in background. Tuttavia, significa anche che puoi chiamare questa query solo dall'interno di una coroutine.
Ed è tutto ciò che devi fare per utilizzare le coroutine in Room. Piuttosto utile.
Coroutines in Retrofit
Vediamo ora come integrare le coroutine con Retrofit. Apri MainNetwork.kt
e modifica 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 fare due cose:
- Aggiungi un modificatore di sospensione alla funzione
- Rimuovi il wrapper
Call
dal tipo restituito. Qui restituiamoString
, ma potresti restituire anche un tipo complesso basato su JSON. Se vuoi comunque fornire l'accesso all'interoResult
di Retrofit, puoi restituireResult<String>
anzichéString
dalla funzione di sospensione.
Retrofit renderà automaticamente le funzioni di sospensione sicure per il thread principale, in modo da poterle chiamare direttamente da Dispatchers.Main
.
Utilizzo di Room e Retrofit
Ora che Room e Retrofit supportano le funzioni di 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? Si è scoperto che l'utilizzo di sospensione e ripristino consente di ridurre notevolmente la lunghezza del codice. Retrofit ci consente di utilizzare tipi restituiti come String
o un oggetto User
qui, anziché un Call
. È sicuro farlo 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.
Ancora meglio, abbiamo eliminato il withContext
. Poiché sia Room che Retrofit forniscono funzioni di sospensione main-safe, è 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 del compilatore che mostrano cosa succederebbe se cambiassi una funzione in sospensione in un progetto reale.
Esamina il progetto e correggi gli errori del compilatore modificando la funzione in modo che venga sospesa. Ecco le soluzioni rapide per ciascun problema:
TestingFakes.kt
Aggiorna i test fittizi per supportare i nuovi modificatori di sospensione.
TitleDaoFake
- Premi Alt+Invio per aggiungere modificatori di sospensione a tutte le funzioni nella gerarchia
MainNetworkFake
- Premi Alt+Invio per aggiungere modificatori di sospensione a tutte le funzioni nella gerarchia
- Sostituisci
fetchNextTitle
con questa funzione
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Premi Alt+Invio per aggiungere modificatori di sospensione a tutte le funzioni nella gerarchia
- Sostituisci
fetchNextTitle
con questa funzione
override suspend fun fetchNextTitle() = completable.await()
TitleRepository.kt
- Elimina la funzione
refreshTitleWithCallbacks
perché non viene più utilizzata.
Esegui l'app
Esegui di nuovo l'app. Una volta compilata, vedrai che carica i dati utilizzando le coroutine dalla ViewModel a Room e Retrofit.
Congratulazioni, hai completato lo scambio di questa app per utilizzare le 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
è esposta come API pubblica, verrà testata direttamente, mostrando come chiamare le funzioni delle coroutine dai test.
Ecco la funzione refreshTitle
che hai implementato nell'ultimo esercizio:
TitleRepository.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 chiama una funzione di sospensione
Apri TitleRepositoryTest.kt
nella cartella test
, che contiene due TO DO.
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 se non da una coroutine o da un'altra funzione di sospensione e riceverai un errore del compilatore come "La funzione di sospensione refreshTitle deve essere chiamata solo da una coroutine o da un'altra funzione di sospensione".
Il test runner non sa nulla delle coroutine, quindi non possiamo rendere questo test una funzione di sospensione. Potremmo launch
una coroutine utilizzando un CoroutineScope
come in un ViewModel
, tuttavia i test devono eseguire le coroutine fino al completamento prima di essere restituiti. Una volta restituita una funzione di test, il test è terminato. Le coroutine avviate con launch
sono codice asincrono, che potrebbe essere completato in futuro. Pertanto, per testare il codice asincrono, devi indicare al test di attendere il completamento della coroutine. Poiché launch
è una chiamata non bloccante, viene restituita immediatamente e può continuare a eseguire una coroutine dopo la restituzione della funzione, ma non può 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 non andrà a buon fine. La chiamata a launch
verrà restituita immediatamente ed eseguita contemporaneamente al resto dello scenario di test. Il test non ha modo di sapere se refreshTitle
è già stato eseguito o meno e qualsiasi asserzione come la verifica dell'aggiornamento del database sarebbe instabile. Inoltre, se refreshTitle
ha generato un'eccezione, questa non verrà generata nello stack di chiamate di test. Verrà invece inviata al gestore di eccezioni non rilevate di GlobalScope
.
La libreria kotlinx-coroutines-test
ha la funzione runBlockingTest
che blocca le chiamate alle funzioni di sospensione. Quando runBlockingTest
chiama una funzione di sospensione o launches
una nuova coroutine, la esegue immediatamente per impostazione predefinita. Puoi considerarlo un modo per convertire le funzioni di sospensione e le coroutine in normali chiamate di funzione.
Inoltre, runBlockingTest
genererà nuovamente le eccezioni non rilevate. In questo modo è più facile testare quando una coroutine genera un'eccezione.
Implementare un test con una coroutine
Racchiudi la chiamata a refreshTitle
con runBlockingTest
e rimuovi il wrapper GlobalScope.launch
da subject.refreshTitle().
TitleRepositoryTest.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 dati fittizi forniti per verificare che "OK" venga inserito nel database da refreshTitle
.
Quando il test chiama runBlockingTest
, si blocca fino al completamento della coroutine avviata da runBlockingTest
. All'interno, quando chiamiamo refreshTitle
, utilizza il normale meccanismo di sospensione e ripristino per attendere che la riga del database venga aggiunta al nostro fake.
Al termine della coroutine di test, viene restituito runBlockingTest
.
Scrivere un test di timeout
Vogliamo aggiungere un breve timeout alla richiesta di rete. Scriviamo prima il test, poi implementiamo il timeout. Crea un nuovo test:
TitleRepositoryTest.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 MainNetworkCompletableFake
fittizio fornito, che è un fittizio di rete progettato per sospendere i chiamanti finché il test non li riattiva. Quando refreshTitle
tenta di effettuare una richiesta di rete, si blocca per sempre perché vogliamo testare i timeout.
Poi avvia una coroutine separata per chiamare refreshTitle
. Si tratta di una parte fondamentale del test dei timeout: il timeout deve verificarsi in una coroutine diversa da quella creata da runBlockingTest
. In questo modo, possiamo chiamare la riga successiva, advanceTimeBy(5_000)
, che farà avanzare il tempo di 5 secondi e causerà il timeout dell'altra coroutine.
Si tratta di un test di timeout completo che verrà superato una volta implementato il timeout.
Esegui il comando ora e vedi cosa succede:
Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]
Una delle funzionalità di runBlockingTest
è che non consente la perdita di coroutine al termine del test. Se alla fine del test sono presenti coroutine non completate, come la nostra coroutine di lancio, il test non andrà a buon fine.
Aggiungere un timeout
Apri TitleRepository
e aggiungi un timeout di cinque secondi al recupero di rete. Puoi farlo utilizzando la funzione withTimeout
:
TitleRepository.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 tutti vengono superati.
Nel prossimo esercizio imparerai a scrivere funzioni di ordine superiore utilizzando le coroutine.
In questo esercizio, refactorizzerai refreshTitle
in MainViewModel
per utilizzare una funzione di caricamento dei dati generica. In questo modo imparerai a creare funzioni di ordine superiore che utilizzano le coroutine.
L'implementazione attuale di refreshTitle
funziona, ma possiamo creare una coroutine di caricamento dei dati generica che mostri sempre l'indicatore di caricamento. Ciò potrebbe essere utile in un codebase che carica i dati in risposta a diversi eventi e vuole assicurarsi che l'indicatore di caricamento venga visualizzato in modo coerente.
La revisione dell'implementazione attuale di ogni riga, ad eccezione di repository.refreshTitle()
, è boilerplate per mostrare lo spinner e visualizzare gli errori.
// 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 esegui il refactoring di refreshTitle()
per utilizzare questa funzione di ordine superiore.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
Astrarre la logica relativa alla visualizzazione di un indicatore di caricamento e degli errori ci ha permesso di semplificare il codice effettivo necessario per caricare i dati. Mostrare un indicatore di caricamento o un errore è qualcosa che può essere generalizzato facilmente a qualsiasi caricamento di dati, mentre l'origine e la destinazione effettive dei dati devono essere specificate ogni volta.
Per creare questa astrazione, launchDataLoad
accetta un argomento block
che è una funzione lambda sospesa. Una lambda sospesa ti consente di chiamare le funzioni di sospensione. È così che Kotlin implementa i builder di coroutine launch
e runBlocking
che abbiamo utilizzato in questo codelab.
// suspend lambda
block: suspend () -> Unit
Per creare una lambda di sospensione, inizia con la parola chiave suspend
. La freccia della funzione e il tipo restituito Unit
completano la dichiarazione.
Non è necessario dichiarare spesso le proprie espressioni lambda di sospensione, 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.
Che cos'è WorkManager
Android offre molte opzioni per il lavoro in background differibile. Questo esercizio mostra come integrare WorkManager con le coroutine. WorkManager è una libreria compatibile, flessibile e semplice per il lavoro in background differibile. WorkManager è la soluzione consigliata per questi casi d'uso su Android.
WorkManager fa parte di Android Jetpack ed è un componente dell'architettura per il lavoro in background che richiede una combinazione di esecuzione opportunistica e garantita. L'esecuzione opportunistica significa che WorkManager eseguirà il lavoro in background non appena possibile. L'esecuzione garantita significa che WorkManager si occuperà della logica per avviare il lavoro in una serie di situazioni, anche se esci dall'app.
Per questo motivo, WorkManager è una buona scelta per le attività che devono essere completate alla fine.
Ecco alcuni esempi di attività che sfruttano al meglio WorkManager:
- Caricamento dei log in corso
- Applicare filtri alle immagini e salvare l'immagine
- Sincronizzazione periodica dei dati locali con la rete
Utilizzo delle coroutine con WorkManager
WorkManager fornisce diverse implementazioni della classe ListanableWorker
di base per diversi casi d'uso.
La classe Worker più semplice ci consente di eseguire un'operazione sincrona tramite WorkManager. Tuttavia, dopo aver lavorato finora per convertire la nostra base di codice in modo da utilizzare le coroutine e le funzioni di sospensione, il modo migliore per utilizzare WorkManager è tramite la classe CoroutineWorker
che consente di definire la nostra funzione doWork()
come funzione di sospensione.
Per iniziare, apri RefreshMainDataWork
. Estende già CoroutineWorker
e devi implementare doWork
.
All'interno della funzione suspend
doWork
, chiama refreshTitle()
dal repository e restituisci il risultato appropriato.
Dopo aver completato l'attività, il codice avrà questo aspetto:
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()
}
}
Tieni presente che CoroutineWorker.doWork()
è una funzione di sospensione. A differenza della classe Worker
più semplice, questo codice NON viene eseguito sull'executor specificato nella configurazione di WorkManager, ma utilizza il dispatcher nel membro coroutineContext
(per impostazione predefinita Dispatchers.Default
).
Test di CoroutineWorker
Nessun codebase dovrebbe essere completo senza test.
WorkManager mette a disposizione diversi modi per testare le classi Worker
. Per saperne di più sull'infrastruttura di test originale, puoi leggere la documentazione.
WorkManager v2.1 introduce un nuovo set di API per supportare un modo più semplice per testare le classi ListenableWorker
e, di conseguenza, CoroutineWorker. Nel nostro codice utilizzeremo una di queste nuove API: TestListenableWorkerBuilder
.
Per aggiungere il 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 iniziare il test, comunichiamo a WorkManager
la fabbrica in modo da poter inserire la rete fittizia.
Il test stesso utilizza TestListenableWorkerBuilder
per creare il nostro worker, che possiamo poi eseguire chiamando il metodo startWork()
.
WorkManager è solo un esempio di come le coroutine possono essere utilizzate per semplificare la progettazione delle API.
In questo codelab abbiamo trattato le nozioni di base necessarie per iniziare a utilizzare le coroutine nella tua app.
Abbiamo trattato i seguenti argomenti:
- Come integrare le coroutine nelle app per Android sia dalle attività dell'interfaccia utente che da WorkManager per semplificare la programmazione asincrona.
- Come utilizzare le 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 casi testando il comportamento e chiamando direttamente le funzioni suspend
dai test.
Scopri di più
Consulta il codelab "Advanced Coroutines with Kotlin Flow and LiveData" per scoprire di più sull'utilizzo avanzato delle coroutine su Android.
Le coroutine Kotlin hanno molte funzionalità che non sono state trattate in questo codelab. Se ti interessa saperne di più sulle coroutine Kotlin, leggi le guide alle coroutine pubblicate da JetBrains. Consulta anche l'articolo "Migliorare le prestazioni dell'app con le coroutine Kotlin" per altri pattern di utilizzo delle coroutine su Android.