Kotlin coroutines su Android
Una coroutine è un design pattern di concorrenza che puoi usare su Android per semplificare il codice che viene eseguito in modo asincrono.Le coroutines sono state aggiunte a Kotlin nella versione 1.3 e sono basate su concetti consolidati di altre lingue.
Su Android, le coroutines aiutano a gestire compiti 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 hanno riferito di aver visto un aumento della produttività. Questo argomento descrive come puoi usare le coroutine di Kotlin per affrontare questi problemi, permettendoti di scrivere un codice app più pulito e conciso.
Caratteristiche
Coroutines è la nostra soluzione raccomandata per la programmazione asincrona su Android. Le caratteristiche degne di nota sono le seguenti:
- Leggero: È possibile eseguire molte coroutine su un singolo thread grazie al supporto per la sospensione, che non blocca il thread in cui la coroutine è in esecuzione. La sospensione permette di risparmiare memoria rispetto al blocco, pur supportando molte operazioni simultanee.
- Meno perdite di memoria: Usare la concorrenza strutturata per eseguire operazioni all’interno di un ambito.
- Supporto incorporato alla cancellazione: la cancellazione viene propagata automaticamente attraverso la gerarchia delle coroutine in esecuzione.
- Integrazione con Jetpack: Molte librerie Jetpack includono estensioni che forniscono un supporto completo alle coroutine. Alcune biblioteche forniscono anche il proprio ambito di coroutine che è possibile utilizzare per una concorrenza strutturata.
Panoramica degli esempi
Basati sulla Guida all’architettura delle app, gli esempi in questo argomento fanno una richiesta di rete e restituiscono il risultato al thread principale, dove l’app può poi mostrare il risultato all’utente.
In particolare, il componente ViewModel
Architettura chiama il livello del repository sul thread principale per attivare la richiesta di rete. Questa guida itera attraverso varie soluzioni che usano le coroutine per mantenere il thread principale sbloccato.
ViewModel
include un insieme di estensioni KTX che lavorano direttamente con le coroutine. Queste estensioni sonolifecycle-viewmodel-ktx
di libreria e sono usate in questa guida.
Informazioni sulla dipendenza
Per usare le coroutine nel tuo progetto Android, aggiungi la seguente dipendenza al file build.gradle
della tua app:
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'}
Esecuzione in un thread in background
Facendo una richiesta di rete sul thread principale, questo aspetta, o si blocca, finché non riceve una risposta. Poiché il thread è bloccato, il sistema operativo non può chiamare onDraw()
, il che causa il blocco dell’applicazione e porta potenzialmente a un dialogo Application Not Responding (ANR). Per una migliore esperienza d’uso, eseguiamo questa operazione su un thread in background.
Prima di tutto, diamo un’occhiata alla nostra classe Repository
e vediamo come fa 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
è sincrona e blocca il thread chiamante. Per modellare la risposta della richiesta di rete, abbiamo la nostra classe Result
.
La ViewModel
attiva la richiesta di rete quando l’utente clicca, per 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
sta bloccando 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) } }}
Secondiamo il codice della coroutine nella funzione login
:
-
viewModelScope
è unaCoroutineScope
predefinita che è inclusa nelleViewModel
estensioni KTX. Si noti che tutte le coroutine devono essere eseguite in ascendenza. UnCoroutineScope
gestisce una o più coroutine correlate. -
launch
è una funzione che crea una coroutine e invia l’esecuzione del suo corpo di funzione al dispatcher corrispondente. -
Dispatchers.IO
indica che questa coroutine dovrebbe essere eseguita su athread riservato alle operazioni di I/O.
La funzione login
viene eseguita come segue:
- L’applicazione chiama la funzione
login
dal livelloView
sul thread principale. -
launch
crea una nuova coroutine, e la richiesta di rete viene fatta indipendentemente su un thread riservato alle operazioni di I/O. - Mentre la coroutine è in esecuzione, la funzione
login
continua l’esecuzione e ritorna, eventualmente prima che la richiesta di rete sia finita. Notate che per semplicità, la risposta della rete è ignorata per ora.
Siccome questa coroutine è iniziata con viewModelScope
, viene eseguita nell’ambito della ViewModel
. Se la ViewModel
viene distrutta perché l’utente sta navigando lontano dallo schermo, viewModelScope
viene automaticamente annullata, e anche tutte le coroutine in esecuzione vengono annullate.
Un problema con l’esempio precedente è che qualsiasi cosa chiamimakeLoginRequest
deve ricordare di spostare esplicitamente l’esecuzione dal thread principale. Vediamo come possiamo modificare la Repository
per risolverci questo problema.
Utilizzare le coroutine per la sicurezza principale
Consideriamo una funzione sicura quando non blocca gli aggiornamenti dell’interfaccia utente sul thread principale. La funzione makeLoginRequest
non è main-safe, poiché chiamandomakeLoginRequest
dal thread principale blocca l’interfaccia utente. Usate la funzionewithContext()
della libreria coroutines per spostare l’esecuzione di una coroutine su un altro thread:
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 su un thread I/O, rendendo la nostra funzione chiamante main-safe e abilitando l’aggiornamento dell’interfaccia utente come necessario.
makeLoginRequest
è anche marcata con la parola chiave suspend
. Questa parola chiave è il modo di Kotlin per imporre che una funzione sia chiamata dall’interno di una coroutine.
Nell’esempio seguente, la coroutine è creata nella LoginViewModel
.Poiché makeLoginRequest
sposta l’esecuzione dal thread principale, la coroutine nella funzione login
può ora 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 } } }}
Nota che la coroutine è ancora necessaria qui, poiché makeLoginRequest
è una funzione suspend
, e tutte le funzioni suspend
devono essere eseguite in una coroutine.
Questo codice differisce dal precedente login
esempio in un paio di modi:
-
launch
non prende unDispatchers.IO
parametro. Quando non si passa unDispatcher
alaunch
, qualsiasi coroutine lanciata daviewModelScope
viene eseguita nel thread principale. - Il risultato della richiesta di rete è ora gestito per visualizzare l’interfaccia utente del fallimento del successore.
La funzione di login ora viene eseguita come segue:
- L’applicazione chiama la funzione
login()
dal livelloView
sul thread principale. -
launch
crea una nuova coroutine per fare la richiesta di rete sul mainthread, e la coroutine inizia l’esecuzione. - Nella coroutine, la chiamata a
loginRepository.makeLoginRequest()
ora sospende l’ulteriore esecuzione della coroutine fino a quando il bloccowithContext
inmakeLoginRequest()
finisce di funzionare. - Una volta che il blocco
withContext
finisce, la coroutine inlogin()
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ò lanciare, usiamo il supporto integrato di Kotlin per le eccezioni.Nell’esempio seguente, usiamo un blocco try-catch
:
class LoginViewModel( private val loginRepository: LoginRepository): ViewModel() { fun makeLoginRequest(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 inaspettata lanciata dalla makeLoginRequest()
chiamata viene gestita come un errore nell’interfaccia utente.
Leave a Reply