Usa Kotlin Coroutines nella tua app Android

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 e Room.
  • 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 e runBlocking 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:

Scarica Zip

... 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.

  1. Se hai scaricato il file ZIP kotlin-coroutines, decomprimi il file.
  2. Apri il progetto coroutines-codelab in Android Studio.
  3. Seleziona il modulo dell'applicazione start.
  4. Fai clic sul pulsante esegui.pngEsegui 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.

  1. MainActivity mostra l'interfaccia utente, registra i listener di clic e può mostrare un Snackbar. Passa gli eventi a MainViewModel e aggiorna lo schermo in base a LiveData in MainViewModel.
  2. MainViewModel gestisce gli eventi in onMainViewClicked e comunicherà a MainActivity utilizzando LiveData.
  3. Executors definisce BACKGROUND, che può eseguire elementi su un thread in background.
  4. 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:

  1. viewModelScope.launch inizierà una coroutina nella viewModelScope. Ciò significa che il processo che abbiamo trasferito a viewModelScope verrà annullato, tutte le attività in questo job/ambito verranno annullate. Se l'utente ha lasciato l'attività prima che delay sia tornato, questa coroutine verrà annullata automaticamente quando viene richiamato onCleared al momento dell'eliminazione di ViewModel.
  2. Poiché viewModelScope ha un supervisore predefinito di Dispatchers.Main, questa stringa verrà avviata nel thread principale. Più avanti vedremo come utilizzare diversi thread.
  3. La funzione delay è una funzione suspend. 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.

  1. InstantTaskExecutorRule è una regola JUnit che configura LiveData per l'esecuzione sincrona di ogni attività
  2. MainCoroutineScopeRule è una regola personalizzata in questo codebase che configura Dispatchers.Main in modo che utilizzi una TestCoroutineDispatcher da kotlinx-coroutines-test. Questo consente ai test di avanzare un orologio virtuale per i test e consente al codice di utilizzare Dispatchers.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

  1. Fai clic con il pulsante destro del mouse sul nome del corso MainViewModelTest nell'editor per aprire un menu contestuale.
  2. Nel menu contestuale scegli esegui.pngEsegui 'MainViewmodelTest'
  3. Per le esecuzioni future, puoi selezionare questa configurazione di test nelle configurazioni accanto al pulsante esegui.png 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.

  1. MainDatabase implementa un database utilizzando Room che salva e carica un Title.
  2. MainNetwork implementa un'API di rete che recupera un nuovo titolo. Usa Retrofit per recuperare i titoli. L'app Retrofit è 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.
  3. TitleRepository implementa una singola API per recuperare o aggiornare il titolo combinando i dati della rete e del database.
  4. 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 esegui.png. 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,

  1. Passa a un altro thread con BACKGROUND ExecutorService
  2. Esegui la richiesta di rete fetchNextTitle utilizzando il metodo execute() di blocco. In questo modo verrà eseguita la richiesta di rete nel thread corrente, in questo caso uno dei thread in BACKGROUND.
  3. Se il risultato è positivo, salvalo nel database con insertTitle e chiama il metodo onCompleted().
  4. 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:

  1. withContext restituisce il risultato al mittente che lo ha chiamato, in questo caso Dispatchers.Main. La versione di callback denominata callback in un thread nel servizio esecutore BACKGROUND.
  2. 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:

  1. Aggiungi un modificatore di sospensione alla funzione
  2. Rimuovi il wrapper Call dal tipo di ritorno. Qui stiamo tornando all'app String, ma potresti restituire anche un tipo complesso basato su json. Se vuoi comunque fornire l'accesso alla versione completa di Result, puoi restituire Result<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

  1. Premi Alt-Invio e modificatori di sospensione per tutte le funzioni nell'area gerarchica

MainNetworkFake

  1. Premi Alt-Invio e modificatori di sospensione per tutte le funzioni nell'area gerarchica
  2. Sostituisci fetchNextTitle con questa funzione
override suspend fun fetchNextTitle() = result

MainNetworkpuòbleFake

  1. Premi Alt-Invio e modificatori di sospensione per tutte le funzioni nell'area gerarchica
  2. 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.