Una coroutine è un pattern di progettazione della concorrenza che puoi utilizzare su Android per semplificare il codice che viene eseguito in modo asincrono. Le coroutine sono state aggiunte a Kotlin nella versione 1.3 e si basano su concetti consolidati di altri linguaggi.
Su Android, le coroutine aiutano a gestire le attività a lunga esecuzione che altrimenti potrebbero bloccare il thread principale e causare la mancata risposta dell'app. Oltre il 50% degli sviluppatori professionisti che utilizzano le coroutine ha segnalato un aumento della produttività. Questo argomento descrive come utilizzare le coroutine Kotlin per risolvere questi problemi, consentendoti di scrivere codice dell'app più pulito e conciso.
Funzionalità
Le coroutine sono la soluzione consigliata per la programmazione asincrona su Android. Le funzionalità degne di nota includono le seguenti:
- Leggerezza: puoi eseguire molte coroutine su un singolo thread grazie al supporto per la sospensione, che non blocca il thread in cui è in esecuzione la coroutine. La sospensione consente di risparmiare memoria rispetto al blocco, supportando al contempo molte operazioni simultanee.
- Meno perdite di memoria: utilizza la concorrenza strutturata per eseguire le operazioni in un ambito.
- Supporto integrato per l'annullamento: L'annullamento viene propagato automaticamente tramite la gerarchia delle coroutine in esecuzione.
- Integrazione di Jetpack: molte librerie Jetpack includono estensioni che forniscono il supporto completo per le coroutine. Alcune librerie forniscono anche il proprio ambito di coroutine che puoi utilizzare per la concorrenza strutturata.
Panoramica degli esempi
In base alla Guida all'architettura dell'app, gli esempi in questo argomento effettuano una richiesta di rete e restituiscono il risultato al thread principale, dove l'app può quindi visualizzarlo all'utente.
In particolare, il ViewModel
componente di architettura chiama il livello del repository sul thread principale per
attivare la richiesta di rete. Questa guida illustra varie soluzioni che utilizzano le coroutine per mantenere il thread principale non bloccato.
ViewModel include un insieme di estensioni KTX che funzionano direttamente con le coroutine. Queste estensioni sono
lifecycle-viewmodel-ktx libreria e vengono utilizzate
in questa guida.
Informazioni sulle dipendenze
Per utilizzare le coroutine nel tuo progetto Android, aggiungi la seguente dipendenza al file build.gradle dell'app:
Alla moda
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
L'invio di una richiesta di rete sul thread principale fa sì che questo attenda, o blocchi,
fino a quando 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.
Innanzitutto, 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>() } private const val loginUrl = "https://example.com/login" class LoginRepository(private val responseParser: LoginResponseParser) { // 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 della richiesta di rete, abbiamo la nostra classe Result.
Il 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'UI 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è unCoroutineScopepredefinito incluso nelle estensioni KTX diViewModel. Tieni presente che tutte le coroutine devono essere eseguite in un ambito. UnCoroutineScopegestisce una o più coroutine correlate.launchè una funzione che crea una coroutine e invia l'esecuzione del corpo della funzione al dispatcher corrispondente.Dispatchers.IOindica che questa coroutine deve essere eseguita su un thread riservato alle operazioni di I/O.
La funzione login viene eseguita come segue:
- L'app chiama la funzione
logindal livelloViewsul thread principale. launchcrea una nuova coroutine e la richiesta di rete viene effettuata in modo indipendente su un thread riservato alle operazioni di I/O.- Mentre la coroutine è in esecuzione, la funzione
logincontinua l'esecuzione e restituisce, possibilmente prima che la richiesta di rete sia terminata. 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 si allontana dalla schermata, viewModelScope viene annullato automaticamente e anche tutte le coroutine in esecuzione vengono annullate.
Un problema con l'esempio precedente è che qualsiasi elemento che chiama makeLoginRequest deve ricordare di spostare esplicitamente l'esecuzione dal thread principale. Vediamo come possiamo modificare Repository per risolvere questo problema.
Utilizzare le coroutine per la sicurezza del thread principale
Consideriamo una funzione sicura per il thread principale quando non blocca gli aggiornamenti dell'UI sul thread principale. La funzione makeLoginRequest non è sicura per il thread principale, poiché la chiamata di makeLoginRequest dal thread principale blocca l'UI. Utilizza la funzione withContext() della libreria delle coroutine per spostare l'esecuzione di una coroutine su 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 su un thread di I/O, rendendo la funzione chiamante sicura per il thread principale e consentendo all'UI di aggiornarsi in base alle esigenze.
makeLoginRequest è anche contrassegnato dalla parola chiave suspend. Questa parola chiave è il modo in cui Kotlin impone che una funzione venga chiamata dall'interno di una coroutine.
Nell'esempio seguente, la coroutine viene creata in 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 */ } } } } }
Tieni presente 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 dall'esempio precedente di login in alcuni modi:
launchnon accetta un parametroDispatchers.IO. Quando non passi unDispatcheralaunch, tutte le coroutine avviate daviewModelScopevengono eseguite nel thread principale.- Il risultato della richiesta di rete viene ora gestito per visualizzare l'UI di successo o errore.
La funzione di accesso viene ora eseguita come segue:
- L'app chiama la funzione
login()dal livelloViewsul thread principale. launchcrea una nuova coroutine sul thread principale e la coroutine inizia l'esecuzione.- All'interno della coroutine, la chiamata a
loginRepository.makeLoginRequest()ora sospende l'ulteriore esecuzione della coroutine fino al termine dell'esecuzione del bloccowithContextinmakeLoginRequest(). - Al termine del blocco
withContext, 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ò generare, utilizza il supporto integrato di Kotlin's
per le eccezioni.
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 nell'UI.
Risorse aggiuntive per le 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:
- Panoramica delle coroutine (JetBrains)
- Guida alle coroutine (JetBrains)
- Risorse aggiuntive per le coroutine e il flusso Kotlin