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. Coroutine sono stati aggiunti a Kotlin nella versione 1.3 e si basano su concetti da altre lingue.

Su Android, le coroutine aiutano a gestire le attività di lunga durata che potrebbero altrimenti il thread principale verrà bloccato e la tua app non risponde. Oltre il 50% degli sviluppatori professionisti che utilizzano le coroutine ha dichiarato di aver visto di produttività in più. Questo argomento descrive come utilizzare le coroutine Kotlin per affrontare questi risolvere problemi, permettendoti di scrivere codice dell'app più chiaro e conciso.

Funzionalità

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

  • Leggera: puoi eseguire molte coroutine in un singolo thread a causa assistenza per sospensione, che non blocca il thread su cui è in esecuzione la coroutine. In fase di sospensione risparmia memoria rispetto al blocco e supporta molte operazioni simultanee.
  • Meno fughe di memoria: utilizza contemporaneità strutturata eseguire operazioni in un ambito.
  • Supporto dell'annullamento integrato: Annullamento viene propagata automaticamente attraverso la gerarchia di coroutine in esecuzione.
  • Integrazione di Jetpack: molte librerie Jetpack includono estensioni che forniscono supporto completo per le coroutine. Alcune le biblioteche offrono anche il loro ambito coroutine che puoi da utilizzare per la contemporaneità strutturata.

Panoramica degli esempi

In base alla Guida all'architettura delle app, gli esempi in questo argomento effettua una richiesta di rete e restituisce il risultato all'istanza in cui l'app può quindi mostrare il risultato all'utente.

In particolare, la ViewModel Il componente architettura chiama il livello di repository sul thread principale a attivare la richiesta di rete. Questa guida esegue l'iterazione di varie soluzioni che utilizzano coroutine per mantenere sbloccato il thread principale.

ViewModel include un insieme di estensioni KTX che funzionano direttamente con coroutine. Queste estensioni sono lifecycle-viewmodel-ktx e vengono utilizzati in questa guida.

Informazioni sulle dipendenze

Per usare le coroutine nel tuo progetto Android, aggiungi la seguente dipendenza al file build.gradle della tua app:

Groovy

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

Quando viene inviata una richiesta di rete sul thread principale, quest'ultimo attende o blocca finché non riceve una risposta. Poiché il thread è bloccato, il sistema operativo non viene in grado di chiamare onDraw(), causando il blocco dell'app e la possibilità apre la finestra di dialogo ANR (L'applicazione non risponde). Per un utente migliore eseguiamo questa operazione su un thread in background.

Innanzitutto, diamo un'occhiata alla lezione Repository e vediamo come si effettuando 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 di chiamata. Per modellare la risposta alla richiesta di rete, abbiamo la nostra classe Result.

L'elemento ViewModel attiva la richiesta di rete quando l'utente fa clic, ad esempio 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 effettuando la richiesta di rete. La soluzione più semplice per spostare l'esecuzione dal thread principale è creare una nuova coroutine ed eseguire la 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 CoroutineScope predefinito incluso in le estensioni KTX ViewModel. Tieni presente che tutte le coroutine devono essere in l'ambito di attività. Un CoroutineScope gestisce una o più coroutine correlate.
  • launch è una funzione che crea una coroutine e invia il dell'esecuzione del proprio corpo della funzione al committente corrispondente.
  • Dispatchers.IO indica che la coroutine deve essere eseguita su una riservato alle operazioni di I/O.

La funzione login viene eseguita come segue:

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

Poiché la coroutina inizia con viewModelScope, viene eseguita in l'ambito di ViewModel. Se ViewModel viene eliminato perché l'utente esce dalla schermata, viewModelScope sta automaticamente annullato, così come tutte le coroutine in esecuzione.

Un problema con l'esempio precedente è che qualsiasi operazione chiamata makeLoginRequest deve ricordarsi di disattivare esplicitamente l'esecuzione nel thread principale. Vediamo come possiamo modificare il Repository per risolvere questo problema per noi.

Usa le coroutine per la massima sicurezza

Una funzione viene considerata main-safe quando non blocca gli aggiornamenti dell'interfaccia utente nella thread principale. La funzione makeLoginRequest non è sicura per la rete principale, in quanto la chiamata makeLoginRequest del thread principale blocca la UI. Utilizza la Funzione withContext() dalla libreria di coroutine per spostare l'esecuzione di una coroutina a 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 per il principale e consentendo all'UI di aggiornarli in base alle esigenze.

makeLoginRequest è contrassegnato anche con la parola chiave suspend. Questa parola chiave è il metodo di Kotlin di forzare l'applicazione di una funzione in modo che venga richiamata dall'interno di una coroutina.

Nell'esempio seguente, la coroutine viene creata in LoginViewModel. Mentre makeLoginRequest sposta l'esecuzione dal thread principale, la coroutine nella funzione login ora può essere eseguito 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 coroutine è ancora necessaria qui, dal momento che makeLoginRequest è una funzione suspend e tutte le funzioni suspend devono essere eseguite in una coroutine.

Questo codice differisce dall'esempio precedente di login per due aspetti:

  • launch non richiede un parametro Dispatchers.IO. Quando non passa Dispatcher a launch, eventuali coroutine lanciate da viewModelScope eseguito nel thread principale.
  • Il risultato della richiesta di rete viene ora gestito per mostrare l'esito positivo o un'interfaccia utente con errori.

La funzione di accesso viene ora eseguita nel seguente modo:

  • L'app chiama la funzione login() dal livello View del thread principale.
  • launch crea una nuova coroutine sul thread principale e inizia la coroutine dell'esecuzione.
  • Nella coroutine, la chiamata a loginRepository.makeLoginRequest() ora sospende ulteriore esecuzione della coroutina fino al withContext blocco in makeLoginRequest() termina l'esecuzione.
  • Al termine del blocco withContext, la coroutine nella squadra login() riprende sul thread principale con il risultato della richiesta di rete.
di Gemini Advanced.

Gestione delle eccezioni

Per gestire le eccezioni che il livello Repository può generare, utilizza il comando supporto integrato per le eccezioni. Nel seguente esempio, 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, le eccezioni impreviste generate dall'elemento makeLoginRequest() viene gestita come un errore nella UI.

Risorse aggiuntive sulle coroutine

Per informazioni più dettagliate sulle coroutine su Android, vai su Migliora le prestazioni dell'app con le coroutine Kotlin.

Per ulteriori risorse sulle coroutine, consulta i seguenti link: