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 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 soluzione consigliata per la programmazione asincrona su Android. Le funzionalità degne di nota includono:
- Leggera: 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 consente di risparmiare memoria sui blocchi e di supportare molte operazioni simultanee.
- Meno perdite di memoria: usa la contemporaneità strutturata per eseguire operazioni in un ambito.
- Supporto integrato per l'annullamento: l'annullamento viene propagato automaticamente tramite la gerarchia di coroutine in esecuzione.
- Integrazione di Jetpack: molte librerie Jetpack includono estensioni che forniscono il supporto completo delle coroutine. Alcune librerie forniscono anche il proprio ambito Coroutine, che puoi utilizzare per la contemporaneità 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 potrà 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 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 sulla dipendenza
Per utilizzare le coroutine nel tuo progetto Android, aggiungi la seguente dipendenza 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 si effettua 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 può chiamare onDraw()
. Di conseguenza, l'app si blocca e potrebbe essere visualizzata 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 sta
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'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 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
è unCoroutineScope
predefinito incluso nelle estensioni KTXViewModel
. Tieni presente che tutte le coroutine devono essere eseguite in un ambito. UnCoroutineScope
gestisce una o più coroutine correlate.launch
è una funzione che crea una coroutine e invia l'esecuzione del suo corpo di funzione al supervisore corrispondente.Dispatchers.IO
indica che questa coroutine deve essere eseguita su un thread riservato per le operazioni di I/O.
La funzione login
viene eseguita come segue:
- L'app chiama la funzione
login
dal livelloView
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 restituisce, probabilmente prima del completamento della richiesta di rete. Tieni presente che, per semplicità, per il momento la risposta della rete viene ignorata.
Poiché questa coroutine viene avviata con viewModelScope
, viene eseguita nell'ambito di ViewModel
. Se l'elemento ViewModel
viene eliminato perché
l'utente esce dallo schermo, viewModelScope
viene annullato
automaticamente e vengono annullate anche tutte le coroutine in esecuzione.
Un problema dell'esempio precedente è che per qualsiasi chiamata a makeLoginRequest
sia necessario ricordare di spostare esplicitamente l'esecuzione fuori dal thread principale. Vediamo come modificare Repository
per risolvere questo problema.
Usa le coroutine per la sicurezza principale
Consideriamo una funzione main-safe quando non blocca gli aggiornamenti dell'interfaccia utente nel
thread principale. La funzione makeLoginRequest
non è sicura per l'ambiente principale, poiché 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 di I/O, rendendo la nostra funzione di chiamata sicura e consentendo all'interfaccia utente di
aggiornarsi in base alle esigenze.
makeLoginRequest
è contrassegnato anche con la parola chiave suspend
. Questa parola chiave
è il modo di Kotlin per forzare una funzione da chiamare 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ò 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 si differenzia dal precedente esempio di login
per due motivi:
launch
non richiede un parametroDispatchers.IO
. Quando non passi unDispatcher
alaunch
, tutte le coroutine lanciate daviewModelScope
vengono eseguite nel thread principale.- Il risultato della richiesta di rete viene ora gestito per visualizzare l'UI di operazione riuscita.
La funzione di accesso ora viene eseguita come segue:
- L'app chiama la funzione
login()
dal livelloView
nel thread principale. launch
crea una nuova coroutine sul thread principale, dove viene avviata l'esecuzione.- All'interno della coroutine, la chiamata a
loginRepository.makeLoginRequest()
ora sospende l'ulteriore esecuzione della coroutine fino al termine del bloccowithContext
inmakeLoginRequest()
. - 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 per le eccezioni di Kotlin.
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, 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 delle app con le coroutine Kotlin.
Per altre risorse sulle coroutine, visita i seguenti link:
- Panoramica delle coroutine (JetBrains)
- Guida alle coroutine (JetBrains)
- Risorse aggiuntive per coroutine e flow Kotlin