Les coroutines Kotlin sur Android
Une coroutine est un modèle de conception de la concurrence que vous pouvez utiliser surAndroid pour simplifier le code qui s’exécute de manière asynchrone.Les coroutines ont été ajoutées à Kotlin dans la version 1.3 et sont basées sur des concepts établis dans d’autres langages.
Sur Android, les coroutines aident à gérer les tâches de longue durée qui pourraient sinon bloquer le thread principal et faire en sorte que votre application ne réponde plus.Plus de 50% des développeurs professionnels qui utilisent les coroutines ont déclaré avoir constaté une augmentation de la productivité.Ce sujet décrit comment vous pouvez utiliser les coroutines Kotlin pour résoudre cesproblèmes, vous permettant d’écrire un code d’application plus propre et plus concis.
Caractéristiques
Les coroutines sont notre solution recommandée pour la programmation asynchrone surAndroid. Les caractéristiques notables sont les suivantes :
- Légèreté : Vous pouvez exécuter de nombreuses coroutines sur un seul thread grâce au support de la suspension, qui ne bloque pas le thread où la coroutine s’exécute. La suspension permet d’économiser de la mémoire par rapport au blocage tout en supportant de nombreuses opérations concurrentes.
- Moins de fuites de mémoire : Utilisez la concurrence structurée pour exécuter des opérations dans une portée.
- Support intégré de l’annulation:L’annulation est propagée automatiquement à travers la hiérarchie de coroutine en cours d’exécution.
- Intégration de Jetpack : De nombreuses bibliothèques Jetpack incluent des extensions qui fournissent un support complet des coroutines. Certaines bibliothèques fournissent également leur propre portée de coroutine que vous pouvez utiliser pour une concurrence structurée.
Synthèse des exemples
Selon le Guide de l’architecture des apps, les exemples de ce sujet effectuent une requête réseau et renvoient le résultat au thread principal, où l’app peut ensuite afficher le résultat à l’utilisateur.
Spécifiquement, le composant ViewModel
Architecture appelle la couche de dépôt sur le thread principal pour déclencher la requête réseau. Ce guide itère à travers diverses solutions qui utilisent les coroutines pour garder le thread principal débloqué.
ViewModel
comprend un ensemble d’extensions KTX qui fonctionnent directement avec lescoroutines. Ces extensions constituentlifecycle-viewmodel-ktx
une bibliothèque et sont utilisées dans ce guide.
Infos sur les dépendances
Pour utiliser les coroutines dans votre projet Android, ajoutez la dépendance suivante au fichier build.gradle
de votre application:
dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9'}
Exécution dans un thread d’arrière-plan
La réalisation d’une requête réseau sur le thread principal entraîne une attente, ou un blocage, jusqu’à ce qu’il reçoive une réponse. Comme le thread est bloqué, le système d’exploitation ne peut pas appeler onDraw()
, ce qui entraîne le gel de votre application et peut conduire à une boîte de dialogue Application Not Responding (ANR). Pour une meilleure expérience d’utilisation, exécutons cette opération sur un thread d’arrière-plan.
D’abord, jetons un coup d’œil à notre classe Repository
et voyons comment elle traite 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 appelant. Pour modéliser la réponse de la requête réseau, nous avons notre propre classe Result
.
La 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 de l’interface utilisateur lorsqu’il effectue la requête réseau. La solution la plus simple pour déplacer l’exécution hors du thread principal est de créer une nouvelle coroutine et d’exécuter la requête réseau sur 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) } }}
Décortiquons le code des coroutines dans la fonction login
:
-
viewModelScope
est uneCoroutineScope
prédéfinie qui est incluse avec les extensionsViewModel
KTX. Notez que toutes les coroutines doivent s’exécuter en ascope. UneCoroutineScope
gère une ou plusieurs coroutines liées. -
launch
est une fonction qui crée une coroutine et dispatche l’exécution de son corps de fonction au dispatcher correspondant. -
Dispatchers.IO
indique que cette coroutine doit être exécutée sur un athread réservé aux opérations d’E/S.
La fonction login
est exécutée de la manière suivante :
- L’application appelle la fonction
login
à partir de la coucheView
sur le thread principal. -
launch
crée une nouvelle coroutine, et la requête réseau est effectuée de manière indépendante sur un thread réservé aux opérations d’E/S. - Alors que la coroutine est en cours d’exécution, la fonction
login
poursuit son exécution et revient, éventuellement avant que la requête réseau ne soit terminée. Notez que pour des raisons de simplicité, la réponse du réseau est ignorée pour le moment.
Comme cette coroutine est lancée avec viewModelScope
, elle est exécutée dans la portée de la ViewModel
. Si le ViewModel
est détruit parce que l’utilisateur navigue loin de 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 ce qui appellemakeLoginRequest
doit se souvenir de déplacer explicitement l’exécution hors du thread principal. Voyons comment nous pouvons modifier le Repository
pour résoudre ce problème pour nous.
Utiliser les coroutines pour la sécurité principale
Nous considérons une fonction comme étant de sécurité principale lorsqu’elle ne bloque pas les mises à jour de l’interface utilisateur sur le thread principal. La fonction makeLoginRequest
n’est pas main-safe, car l’appelmakeLoginRequest
depuis le thread principal bloque l’interface utilisateur. Utilisez la fonctionwithContext()
de la bibliothèque coroutines pour déplacer l’exécution d’une coroutine vers un thread différent:
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’entrée/sortie, rendant notre fonction d’appel main-safe et permettant à l’interface utilisateur de se mettre à jour si nécessaire.
makeLoginRequest
est également marqué par le mot-clé suspend
. Ce mot-clé est la façon de Kotlin d’imposer qu’une fonction soit appelée à partir d’une coroutine.
Dans l’exemple suivant, la coroutine est créée dans le LoginViewModel
.Comme makeLoginRequest
déplace l’exécution hors du thread principal, la coroutine dans la fonction login
peut maintenant ê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, puisque makeLoginRequest
est une fonction suspend
, et que toutes les fonctions suspend
doivent être exécutées dans une coroutine.
Ce code diffère de l’exemple login
précédent de plusieurs façons :
-
launch
ne prend pas de paramètreDispatchers.IO
. Lorsque vous ne passez pas unDispatcher
àlaunch
, toutes les coroutines lancées à partir deviewModelScope
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 d’échec du successeur.
La fonction de connexion s’exécute maintenant comme suit :
- L’application appelle la fonction
login()
de la coucheView
sur le thread principal. -
launch
crée une nouvelle coroutine pour faire la demande de réseau sur le mainthread, et la coroutine commence son exécution. - Dans la coroutine, l’appel à
loginRepository.makeLoginRequest()
maintenant suspend la poursuite de l’exécution de la coroutine jusqu’à ce que le blocwithContext
dansmakeLoginRequest()
finisse de s’exécuter. - Une fois le bloc
withContext
terminé, la coroutine danslogin()
reprend son exécution sur le fil principal avec le résultat de la requête réseau.
Gestion des exceptions
Pour gérer les exceptions que la couche Repository
peut lancer, utilisez le support intégré des exceptions de Kotlin.Dans l’exemple suivant, nous utilisons un bloc 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 } } }}
Dans cet exemple, toute exception inattendue lancée par l’appel makeLoginRequest()
est gérée comme une erreur dans l’interface utilisateur.
Leave a Reply