Une coroutine est un modèle de conception de simultanéité que vous pouvez utiliser sur Android pour simplifier le code qui s'exécute de manière asynchrone. Coroutines ont été ajoutés à Kotlin dans la version 1.3 et sont basés sur des concepts d'autres langages.
Sur Android, les coroutines permettent de gérer les tâches de longue durée qui pourraient autrement bloquer le thread principal et entraîner le blocage de votre application. Plus de 50 % des développeurs professionnels qui utilisent des coroutines ont constaté une augmentation de la productivité. Cet article décrit comment vous pouvez utiliser les coroutines Kotlin pour résoudre ces problèmes et écrire du code plus clair et plus concis pour vos applications.
Fonctionnalités
Les coroutines sont la solution recommandée pour la programmation asynchrone sur Android. Les fonctionnalités les plus importantes sont les suivantes :
- Légèreté : vous pouvez exécuter de nombreuses coroutines sur un seul thread grâce à la prise en charge de la suspension, qui ne bloque pas le thread dans lequel la coroutine est exécutée. La suspension permet de libérer de la mémoire contrairement au blocage, tout en prenant en charge plusieurs opérations simultanées.
- Moins de fuites de mémoire: utilisez simultanéité structurée pour exécuter des opérations dans un champ d’application.
- Résiliation intégrée: Résiliation se propage automatiquement dans la hiérarchie des coroutines en cours d'exécution.
- Intégration Jetpack : de nombreuses bibliothèques Jetpack incluent des extensions compatibles avec toutes les coroutines. Certaines bibliothèques fournissent également leur propre champ d'application de coroutine que vous pouvez utiliser pour la simultanéité structurée.
Présentation des exemples
Sur la base du guide de l'architecture des applications, les exemples de cet article effectuent une requête réseau et renvoient le résultat au thread principal, où l'application peut ensuite afficher le résultat à l'utilisateur.
Plus précisément, le composant d'architecture ViewModel
appelle la couche du dépôt sur le thread principal pour déclencher la requête réseau. Ce guide passe en revue différentes solutions
qui utilisent des coroutines pour maintenir le thread principal débloqué.
ViewModel
inclut un ensemble d'extensions KTX qui fonctionnent directement avec les coroutines. Ce sont les extensions de la bibliothèque lifecycle-viewmodel-ktx
que nous utilisons dans ce guide.
Informations sur les dépendances
Pour utiliser des coroutines dans votre projet Android, ajoutez la dépendance suivante au fichier build.gradle
de votre application :
Groovy
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9' }
Kotlin
dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9") }
Exécuter sur un thread d'arrière-plan
Effectuer une requête réseau sur le thread principal le met en attente ou le bloque jusqu'à ce qu'il reçoive une réponse. Comme le thread est bloqué, l'OS ne peut pas appeler onDraw()
, ce qui provoque le blocage de votre application et peut entraîner l'affichage d'une boîte de dialogue "L'application ne répond pas" (ANR). Pour améliorer l'expérience utilisateur, exécutons cette opération sur un thread d'arrière-plan.
Tout d'abord, examinons la classe Repository
et voyons comment elle envoie la requête réseau :
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
est synchrone et bloque le thread d'appel. Pour modéliser la réponse à la requête réseau, nous disposons de notre propre classe Result
.
ViewModel
déclenche la requête réseau lorsque l'utilisateur clique, par exemple, sur un bouton :
class LoginViewModel(
private val loginRepository: LoginRepository
): ViewModel() {
fun login(username: String, token: String) {
val jsonBody = "{ username: \"$username\", token: \"$token\"}"
loginRepository.makeLoginRequest(jsonBody)
}
}
Avec le code précédent, LoginViewModel
bloque le thread UI lors de l'envoi de la requête réseau. La solution la plus simple pour déplacer l'exécution en dehors du thread principal consiste à créer une coroutine et à exécuter la requête réseau sur un thread d'E/S :
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)
}
}
}
Analysons le code des coroutines dans la fonction login
:
viewModelScope
est unCoroutineScope
prédéfini qui est inclus dans les extensions KTX duViewModel
. Notez que toutes les coroutines doivent s'exécuter dans un champ d'application. UnCoroutineScope
gère une ou plusieurs coroutines associées.launch
est une fonction qui crée une coroutine et envoie l'exécution de son corps de fonction au coordinateur correspondant.Dispatchers.IO
indique que cette coroutine doit être exécutée sur un thread réservé aux opérations d'E/S.
La fonction login
est exécutée comme suit :
- L'application appelle la fonction
login
à partir de la coucheView
sur le thread principal. launch
crée une coroutine, et la requête réseau est effectuée indépendamment sur un thread réservé aux opérations d'E/S.- Pendant que la coroutine est en cours d'exécution, la fonction
login
continue de s'exécuter et renvoie un résultat, éventuellement avant la fin de la requête réseau. Notez que pour plus de simplicité, la réponse réseau est ignorée pour le moment.
Comme cette coroutine est démarrée avec viewModelScope
, elle est exécutée dans le champ d'application du ViewModel
. Si ViewModel
est détruit, car l'utilisateur quitte l'écran, viewModelScope
est automatiquement annulé, et toutes les coroutines en cours d'exécution sont également annulées.
Un problème avec l'exemple précédent est que tout appel à makeLoginRequest
doit déplacer explicitement l'exécution en dehors du thread principal. Voyons comment modifier Repository
pour résoudre ce problème.
Utiliser des coroutines pour la sécurité principale
Nous considérons qu'une fonction est sécurisée lorsqu'elle ne bloque pas les mises à jour de l'interface utilisateur sur le thread principal. La fonction makeLoginRequest
n'est pas sécurisée, car si makeLoginRequest
appelle à partir du thread principal, cela bloque l'interface utilisateur. Utilisez la fonction withContext()
de la bibliothèque de coroutines pour déplacer l'exécution d'une coroutine vers un autre 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)
déplace l'exécution de la coroutine vers un thread d'E/S, ce qui renforce la sécurité de notre fonction d'appel et permet à l'interface utilisateur de se mettre à jour si nécessaire.
makeLoginRequest
est également marqué avec le mot clé suspend
. Ce mot clé permet à Kotlin d'appliquer une fonction à partir d'une coroutine.
Dans l'exemple suivant, la coroutine est créée dans le LoginViewModel
.
Lorsque makeLoginRequest
déplace l'exécution en dehors du thread principal, la coroutine de la fonction login
peut désormais être exécutée dans le thread principal :
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
}
}
}
}
Notez que la coroutine est toujours nécessaire ici, car makeLoginRequest
est une fonction suspend
, et toutes les fonctions suspend
doivent être exécutées dans une coroutine.
Ce code diffère de l'exemple précédent de login
de deux manières :
launch
n'accepte pas le paramètreDispatchers.IO
. Lorsque vous ne transmettez pas deDispatcher
àlaunch
, toutes les coroutines lancées à partir duviewModelScope
s'exécutent dans le thread principal.- Le résultat de la requête réseau est maintenant géré pour afficher l'interface utilisateur de réussite ou d'échec.
La fonction de connexion s'exécute désormais comme suit :
- L'application appelle la fonction
login()
à partir de la coucheView
sur le thread principal. launch
crée une coroutine sur le thread principal, qui commence à s'exécuter.- Dans la coroutine, l'appel de
loginRepository.makeLoginRequest()
suspend l'exécution de la coroutine jusqu'à la fin de l'exécution du blocwithContext
dansmakeLoginRequest()
. - Une fois l'exécution du bloc
withContext
terminée, la coroutine delogin()
reprend l'exécution sur le thread principal avec le résultat de la requête réseau.
Gérer les exceptions
Pour gérer les exceptions que la couche Repository
peut générer, utilisez la compatibilité intégrée avec les exceptions de Kotlin.
Dans l'exemple suivant, nous utilisons un bloc 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
}
}
}
}
Dans cet exemple, toute exception inattendue générée par l'appel makeLoginRequest()
est traitée comme une erreur dans l'interface utilisateur.
Ressources supplémentaires sur les coroutines
Pour en savoir plus sur les coroutines sur Android, consultez la page Améliorer les performances des applications avec les coroutines Kotlin.
Pour plus de ressources sur les coroutines, consultez les liens suivants :
- Présentation des coroutines (JetBrains)
- Guide des coroutines (JetBrains)
- Ressources supplémentaires sur les coroutines Kotlin et Flow