Una coroutine è un pattern di progettazione della contemporaneità 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 affermati in altre lingue.
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 usano coroutine ha dichiarato di aver osservato un aumento della produttività. Questo argomento descrive come utilizzare le coroutine Kotlin per risolvere questi problemi, consentendoti di scrivere 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:
- Leggera: puoi eseguire molte coroutine su un singolo thread grazie al supporto per la sospensione, che Blocca il thread in cui è in esecuzione la coroutine. La sospensione consente di risparmiare memoria rispetto al blocco e supporta molte operazioni in parallelo.
- Meno perdite di memoria: usa la contemporaneità strutturata per eseguire operazioni in un ambito.
- Supporto della cancellazione integrata: la cancellazione viene propagata automaticamente attraverso la gerarchia coroutine in esecuzione.
- Integrazione con Jetpack: molte librerie Jetpack includono estensioni che offrono supporto completo per le cartelle. Alcune librerie forniscono anche il loro ambito Coroutine 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 ViewModel
Architecture chiama il livello del repository sul thread principale per
attivare la richiesta di rete. Questa guida ripete le varie soluzioni che utilizzano le coroutine per tenere sbloccato il thread principale.
ViewModel
include un insieme di estensioni KTX che funzionano direttamente con le Coroutine. Queste estensioni sono una raccolta di lifecycle-viewmodel-ktx
e vengono utilizzate in questa guida.
Informazioni sulle dipendenze
Per utilizzare le coppie nel progetto Android, aggiungi la seguente dipendenza al file build.gradle
dell'app:
Scadente
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
Esecuzione di un thread in background in corso...
Quando si effettua una richiesta di rete nel thread principale, si verifica l'attesa o il blocco finché la risposta non riceve una risposta. Poiché il thread è bloccato, il sistema operativo non può chiamare onDraw()
, il che comporta il blocco dell'app e potenzialmente la visualizzazione di una finestra di dialogo che non risponde (ANR) dell'applicazione. Per una migliore esperienza utente, eseguiamo questa operazione su un thread in background.
Innanzitutto, diamo un'occhiata alla nostra 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
.
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
esegue 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 coroutine nella funzione login
:
viewModelScope
è unCoroutineScope
predefinito incluso nelle estensioni KTX diViewModel
. Tieni presente che tutte le coroutine devono essere eseguite in un ambito.CoroutineScope
gestisce una o più coroutine correlate.launch
è una funzione che crea una coroutine e invia l'esecuzione del suo corpo funzione al committente 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 su un thread riservato alle operazioni di I/O.- Mentre la coroutine è in esecuzione, la funzione
login
continua l'esecuzione e restituisce, possibilmente prima che la richiesta di rete sia completata. Tieni presente che, per semplicità, la risposta di rete per ora 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 vengono annullati anche tutti i canali in esecuzione.
Un problema con l'esempio precedente è che qualsiasi chiamata a makeLoginRequest
deve ricordare di spostare esplicitamente l'esecuzione dal thread principale. Vediamo come possiamo modificare Repository
per risolvere questo problema.
Utilizza le coroutine per la sicurezza principale
Consideriamo una funzione main-safe quando non blocca gli aggiornamenti dell'interfaccia utente sul thread principale. La funzione makeLoginRequest
non è sicura per la principale, poiché la chiamata
makeLoginRequest
dal thread principale blocca l'interfaccia utente. Utilizza la funzione withContext()
della libreria di coroutine per spostare l'esecuzione di una coroutine in 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 in un thread di I/O, rendendo la nostra funzione di chiamata sicura per la principale e consentendo l'aggiornamento dell'interfaccia utente in base alle necessità.
Anche makeLoginRequest
è contrassegnato con la parola chiave suspend
. Questa parola chiave è il modo di Kotlin per applicare una funzione da chiamare all'interno di una coroutine.
Nell'esempio seguente, la coroutine viene creata in LoginViewModel
.
Mentre makeLoginRequest
sposta l'esecuzione fuori 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 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 per login
in due modi:
launch
non accetta un parametroDispatchers.IO
. Quando non superi unDispatcher
inlaunch
, le query create daviewModelScope
vengono eseguite nel thread principale.- Il risultato della richiesta di rete viene ora gestito in modo da visualizzare la UI riuscita o non 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 nel 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 bloccowithContext
inmakeLoginRequest()
. - Una volta terminato il 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.
Nell'esempio seguente viene utilizzato 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 imprevista generata dalla chiamata makeLoginRequest()
viene gestita come errore nella UI.
Risorse aggiuntive relative alle coroutine
Per uno sguardo più dettagliato sulle coroutine su Android, consulta Migliorare le prestazioni delle app con le coroutine Kotlin.
Per ulteriori risorse correlate alle risorse, consulta i seguenti link: