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 ViewModelArchitecture 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 une CoroutineScope prédéfinie qui est incluse avec les extensions ViewModel KTX. Notez que toutes les coroutines doivent s’exécuter en ascope. Une CoroutineScope 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 couche View 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ètre Dispatchers.IO. Lorsque vous ne passez pas un Dispatcher à 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 couche View 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 bloc withContext dans makeLoginRequest() finisse de s’exécuter.
  • Une fois le bloc withContext terminé, la coroutine dans login() 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