Coroutine Kotlin su Android

Una coroutine è un pattern di progettazione di contemporaneità che puoi utilizzare su Android per semplificare il codice eseguito in modo asincrono. Le coroutine sono state aggiunte a Kotlin nella versione 1.3 e si basano su concetti consolidati di altre lingue.

Su Android, le coroutine aiutano a gestire le attività di lunga durata che potrebbero altrimenti bloccare il thread principale e causare la mancata risposta dell'app. Oltre il 50% degli sviluppatori professionisti che usano le coroutine ha riferito di aver registrato un aumento della produttività. Questo argomento descrive come utilizzare le coroutine Kotlin per risolvere questi problemi, in modo da scrivere un codice dell'app più chiaro e conciso.

Funzionalità

Coroutines è la nostra soluzione consigliata per la programmazione asincrona su Android. Le funzionalità degne di nota includono:

  • Leggero: puoi eseguire molte coroutine su un singolo thread grazie al supporto della sospensione, che non blocca il thread su cui è in esecuzione la coroutine. La sospensione fa risparmiare memoria piuttosto che bloccarla, supportando al contempo molte operazioni in parallelo.
  • Meno perdite di memoria: utilizza la contemporaneità strutturata per eseguire operazioni all'interno di un ambito.
  • Supporto integrato per l'annullamento: l'annullamento viene propagato automaticamente attraverso la gerarchia di coroutine in esecuzione.
  • Integrazione con Jetpack: molte librerie Jetpack includono estensioni che supportano completamente le coroutine. Alcune librerie forniscono anche il proprio ambito a corona che puoi utilizzare per la contemporaneità strutturata.

Panoramica degli esempi

In base alla Guida all'architettura delle app, gli esempi in questo argomento effettuano una richiesta di rete e restituiscono il risultato al thread principale, dove l'app può quindi mostrare il risultato all'utente.

In particolare, il componente Architettura ViewModel chiama il livello repository sul thread principale per attivare la richiesta di rete. Questa guida ripercorre varie soluzioni che usano le coroutine per mantenere sbloccato il thread principale.

ViewModel include un set di estensioni KTX che funzionano direttamente con le coroutine. Queste estensioni sono librerie lifecycle-viewmodel-ktx e vengono utilizzate in questa guida.

Informazioni sulle dipendenze

Per utilizzare le coroutine nel tuo progetto Android, aggiungi la dipendenza seguente al file build.gradle dell'app:

Trendy

dependencies {
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'
}

Kotlin

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9")
}

Esecuzione in un thread in background

Se effettui una richiesta di rete nel thread principale, quest'ultimo attende o blocca finché non riceve una risposta. Poiché il thread è bloccato, il sistema operativo non è in grado di chiamare onDraw(), il che causa il blocco dell'app e potenzialmente la visualizzazione di una finestra di dialogo L'applicazione non risponde (ANR). Per una migliore esperienza utente, eseguiamo questa operazione su un thread in background.

Per prima cosa, diamo un'occhiata alla classe Repository e vediamo come effettua la richiesta di rete:

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())
            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

makeLoginRequest è sincrono e blocca il thread chiamante. Per modellare la risposta alla richiesta di rete, abbiamo la nostra classe Result.

ViewModel attiva la richiesta di rete quando l'utente fa clic, ad esempio, su un pulsante:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Con il codice precedente, LoginViewModel blocca il thread dell'interfaccia utente quando effettua la richiesta di rete. La soluzione più semplice per spostare l'esecuzione dal thread principale è creare una nuova coroutine ed eseguire la richiesta di rete su un thread di I/O:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        // Create a new coroutine to move the execution off the UI thread
        viewModelScope.launch(Dispatchers.IO) {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            loginRepository.makeLoginRequest(jsonBody)
        }
    }
}

Analizziamo il codice delle coroutine nella funzione login:

  • viewModelScope è un valore CoroutineScope predefinito incluso nelle estensioni KTX ViewModel. Tieni presente che tutte le coroutine devono essere eseguite in un ambito. Un CoroutineScope gestisce una o più coroutine correlate.
  • launch è una funzione che crea una coroutina e invia l'esecuzione del corpo della funzione al supervisore corrispondente.
  • Dispatchers.IO indica che questa coroutine deve essere eseguita su un thread riservato alle operazioni di I/O.

La funzione login viene eseguita nel seguente modo:

  • L'app chiama la funzione login dal livello View nel thread principale.
  • launch crea una nuova coroutine e la richiesta di rete viene effettuata in modo indipendente da un thread riservato alle operazioni di I/O.
  • Mentre la coroutine è in esecuzione, la funzione login continua l'esecuzione e la restituisce, probabilmente prima del completamento della richiesta di rete. Tieni presente che, per semplicità, per il momento la risposta di rete viene ignorata.

Poiché questa coroutine viene avviata con viewModelScope, viene eseguita nell'ambito di ViewModel. Se ViewModel viene eliminato perché l'utente esce dallo schermo, viewModelScope viene annullato automaticamente e anche tutte le coroutine in esecuzione vengono annullate.

Un problema dell'esempio precedente è che qualsiasi chiamata a makeLoginRequest deve ricordarsi di spostare esplicitamente l'esecuzione fuori dal thread principale. Vediamo come possiamo modificare Repository per risolvere il problema.

Usa le coroutine per la sicurezza principale

La funzione viene considerata main-safe quando non blocca gli aggiornamenti dell'interfaccia utente nel thread principale. La funzione makeLoginRequest non è sicura per l'ambiente principale, in quanto la chiamata a makeLoginRequest dal thread principale blocca l'interfaccia utente. Usa la funzione withContext() dalla libreria delle coroutine per spostare l'esecuzione di una coroutine in un thread diverso:

class LoginRepository(...) {
    ...
    suspend fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {

        // Move the execution of the coroutine to the I/O dispatcher
        return withContext(Dispatchers.IO) {
            // Blocking network request code
        }
    }
}

withContext(Dispatchers.IO) sposta l'esecuzione della coroutine in un thread I/O, rendendo la nostra funzione di chiamata sicura come principale e abilitando l'aggiornamento della UI in base alle esigenze.

makeLoginRequest è contrassegnato anche con la parola chiave suspend. Questa parola chiave è il modo di Kotlin per far sì che una funzione venga chiamata dall'interno di una coroutine.

Nell'esempio seguente, la coroutine viene creata in LoginViewModel. Mentre makeLoginRequest sposta l'esecuzione dal thread principale, la coroutine nella funzione login può essere eseguita nel thread principale:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {

        // Create a new coroutine on the UI thread
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"

            // Make the network call and suspend execution until it finishes
            val result = loginRepository.makeLoginRequest(jsonBody)

            // Display result of the network request to the user
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Tieni presente che la coroutina è ancora necessaria qui, poiché makeLoginRequest è una funzione suspend e tutte le funzioni suspend devono essere eseguite in una coroutine.

Questo codice differisce dal precedente esempio di login per due motivi:

  • launch non richiede un parametro Dispatchers.IO. Quando non passi un Dispatcher a launch, eventuali coroutine lanciate da viewModelScope vengono eseguite nel thread principale.
  • Il risultato della richiesta di rete viene ora gestito per visualizzare l'interfaccia utente di operazione riuscita.

La funzione di accesso ora viene eseguita nel seguente modo:

  • L'app chiama la funzione login() dal livello View nel thread principale.
  • launch crea una nuova coroutine nel thread principale e la coroutine avvia l'esecuzione.
  • All'interno della coroutina, la chiamata a loginRepository.makeLoginRequest() ora sospende l'ulteriore esecuzione della coroutine fino al termine del blocco withContext in makeLoginRequest().
  • Una volta terminato il blocco withContext, la coroutine in login() riprende l'esecuzione sul thread principale con il risultato della richiesta di rete.

Gestione delle eccezioni

Per gestire le eccezioni che il livello Repository può generare, utilizza il supporto integrato per le eccezioni di Kotlin. Nell'esempio seguente, utilizziamo un blocco try-catch:

class LoginViewModel(
    private val loginRepository: LoginRepository
): ViewModel() {

    fun login(username: String, token: String) {
        viewModelScope.launch {
            val jsonBody = "{ username: \"$username\", token: \"$token\"}"
            val result = try {
                loginRepository.makeLoginRequest(jsonBody)
            } catch(e: Exception) {
                Result.Error(Exception("Network request failed"))
            }
            when (result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

In questo esempio, qualsiasi eccezione imprevista generata dalla chiamata makeLoginRequest() viene gestita come un errore nella UI.

Risorse aggiuntive sulle coroutine

Per un'analisi più dettagliata delle coroutine su Android, consulta Migliorare le prestazioni dell'app con le coroutine Kotlin.

Per altre risorse sulle coroutine, consulta i seguenti link: