Présentation des coroutines dans Kotlin Playground

1. Avant de commencer

Cet atelier de programmation vous présente la simultanéité, un concept essentiel que les développeurs Android doivent maîtriser pour proposer une expérience utilisateur de qualité. La simultanéité implique l'exécution de plusieurs tâches en même temps dans votre application. Par exemple, votre application peut obtenir des données à partir d'un serveur Web ou enregistrer des données utilisateur sur l'appareil, tout en répondant aux événements d'entrée utilisateur et en mettant à jour l'UI en conséquence.

Pour réaliser des tâches simultanées dans votre application, vous utiliserez des coroutines Kotlin. Les coroutines permettent de suspendre l'exécution d'un bloc de code, puis de la reprendre plus tard, afin que d'autres tâches puissent être effectuées en attendant. Les coroutines facilitent l'écriture de code asynchrone, ce qui signifie qu'une tâche n'a pas besoin de se terminer complètement avant de lancer la tâche suivante, ce qui permet à plusieurs tâches de s'exécuter simultanément.

Cet atelier de programmation présente quelques exemples de base dans Kotlin Playground, où vous vous entrainerez à utiliser les coroutines pour mieux maîtriser la programmation asynchrone.

Conditions préalables

  • Savoir créer un programme Kotlin de base avec une fonction main()
  • Connaître les bases du langage Kotlin, y compris les fonctions et les lambdas

Objectifs de l'atelier

  • Créer un programme Kotlin court pour apprendre et tester les principes de base des coroutines

Points abordés

  • Comment les coroutines Kotlin peuvent simplifier la programmation asynchrone
  • Objectif de la simultanéité structurée et son importance

Ce dont vous avez besoin

2. Code synchrone

Programme simple

Dans le code synchrone, une seule tâche conceptuelle est en cours à la fois. Vous pouvez le considérer comme un chemin linéaire séquentiel. Une tâche doit se terminer complètement avant que la suivante ne commence. Vous trouverez ci-dessous un exemple de code synchrone.

  1. Ouvrez Kotlin Playground.
  2. Remplacez le code par le code suivant pour un programme qui affiche une prévision météo ensoleillée. Dans la fonction main(), nous affichons d'abord le texte Weather forecast. Ensuite, nous affichons Sunny.
fun main() {
    println("Weather forecast")
    println("Sunny")
}
  1. Exécutez le code. La sortie de l'exécution du code ci-dessus doit être la suivante :
Weather forecast
Sunny

println() est un appel synchrone, car la tâche d'affichage du texte dans la sortie est terminée avant que l'exécution puisse passer à la ligne de code suivante. Comme chaque appel de fonction dans main() est synchrone, l'ensemble de la fonction main() est synchrone. Le fait qu'une fonction soit synchrone ou asynchrone est déterminé par les parties qui la composent.

Une fonction synchrone n'est renvoyée qu'une fois sa tâche complètement terminée. Ainsi, après l'exécution de la dernière instruction d'affichage dans main(), tout le travail est terminé. La fonction main() est renvoyée et le programme se termine.

Ajouter un délai

Imaginons à présent que l'obtention d'une prévision météo ensoleillée nécessite une requête réseau à un serveur Web distant. Simulez la requête réseau en ajoutant un délai dans le code avant d'afficher que la prévision météo est ensoleillée.

  1. Tout d'abord, ajoutez import kotlinx.coroutines.* en haut de votre code avant la fonction main(). Cela importe des fonctions que vous utiliserez à partir de la bibliothèque de coroutines Kotlin.
  2. Modifiez votre code pour ajouter un appel à delay(1000), ce qui retarde l'exécution du reste de la fonction main() de 1000 millisecondes, soit une seconde. Insérez cet appel delay() avant l'instruction d'impression de Sunny.
import kotlinx.coroutines.*

fun main() {
    println("Weather forecast")
    delay(1000)
    println("Sunny")
}

delay() est en réalité une fonction de suspension spéciale fournie par la bibliothèque de coroutines Kotlin. L'exécution de la fonction main() est suspendue (ou mise en pause) à ce stade, puis reprend une fois le délai spécifié dépassé (une seconde dans ce cas).

Si vous essayez d'exécuter votre programme à ce stade, l'erreur de compilation suivante s'affichera : Suspend function 'delay' should be called only from a coroutine or another suspend function.

Pour apprendre les coroutines dans Kotlin Playground, vous pouvez encapsuler votre code existant avec un appel à la fonction runBlocking() depuis la bibliothèque de coroutines. runBlocking() exécute une boucle d'événements qui peut gérer plusieurs tâches à la fois en poursuivant chaque tâche là où elle s'est arrêtée lorsqu'elle est prête à être reprise.

  1. Déplacez le contenu existant de la fonction main() dans le corps de l'appel runBlocking {}. Le corps de runBlocking{} sera exécuté dans une nouvelle coroutine.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        delay(1000)
        println("Sunny")
    }
}

runBlocking() est synchrone. Il ne sera renvoyé qu'une fois toutes les tâches de son bloc lambda effectuées. Cela signifie qu'il attend que l'opération de l'appel delay() se termine (jusqu'à ce qu'une seconde se soit écoulée), puis continue à exécuter l'instruction d'affichage Sunny. Une fois tout le travail de la fonction runBlocking() terminé, la fonction est renvoyée, ce qui met fin au programme.

  1. Exécutez le programme. Voici la sortie :
Weather forecast
Sunny

La sortie est la même qu'avant. Le code est toujours synchrone. Il s'exécute en ligne droite et n'effectue qu'une seule action à la fois. Toutefois, la différence réside maintenant dans le fait qu'il s'exécute sur une période plus longue en raison du délai.

Le "co" de coroutine signifie coopératif. Le code coopère pour partager la boucle d'événements sous-jacente lorsqu'elle est suspendue afin d'attendre quelque chose, ce qui permet d'exécuter d'autres tâches en attendant. (La partie "routine" de coroutine désigne un ensemble d'instructions, comme une fonction.) Dans cet exemple, la coroutine est suspendue lorsqu'elle atteint l'appel delay(). D'autres tâches peuvent être effectuées en une seconde lorsque la coroutine est suspendue (même si, dans ce programme, aucune autre tâche n'est nécessaire). Une fois le délai écoulé, la coroutine reprend l'exécution et peut continuer à afficher Sunny sur la sortie.

Fonctions de suspension

Si la logique d'exécution de la requête réseau pour obtenir les données météorologiques devient plus complexe, vous pouvez extraire cette logique dans sa propre fonction. Refactorisons le code pour voir son effet.

  1. Extrayez le code qui simule la requête réseau pour récupérer les données météorologiques, puis déplacez-le dans sa propre fonction appelée printForecast(). Appelez printForecast() à partir du code runBlocking().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

fun printForecast() {
    delay(1000)
    println("Sunny")
}

Si vous exécutez le programme maintenant, vous verrez la même erreur de compilation que vous avez vue précédemment. Une fonction de suspension ne peut être appelée qu'à partir d'une coroutine ou d'une autre fonction de suspension. Vous devez donc définir printForecast() comme fonction suspend.

  1. Ajoutez le modificateur suspend juste avant le mot clé fun dans la déclaration de la fonction printForecast() pour en faire une fonction de suspension.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

N'oubliez pas que delay() est une fonction de suspension et que vous venez également de faire de printForecast() une fonction de suspension.

Une fonction de suspension est semblable à une fonction standard, mais elle peut être suspendue et réactivée ultérieurement. Pour ce faire, les fonctions de suspension ne peuvent être appelées que depuis d'autres fonctions de suspension qui rendent cette fonctionnalité disponible.

Une fonction de suspension peut contenir zéro ou plusieurs points de suspension. Un point de suspension est l'endroit au sein de la fonction où l'exécution de la fonction peut être suspendue. Une fois que l'exécution reprend, elle repart d'où elle s'était arrêtée dans le code et exécute le reste de la fonction.

  1. Entraînez-vous à ajouter une autre fonction de suspension à votre code, sous la déclaration de la fonction printForecast(). Appelez cette nouvelle fonction de suspension printTemperature(). Vous pouvez prétendre qu'il s'agit d'une requête réseau pour obtenir les données de température pour les prévisions météorologiques.

Dans la fonction, retardez également l'exécution de 1000 millisecondes, puis affichez une valeur de température sur la sortie, telle que 30 degrés Celsius. Vous pouvez utiliser la séquence d'échappement "\u00b0" pour afficher le symbole degré °.

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Appelez la nouvelle fonction printTemperature() à partir de votre code runBlocking() dans la fonction main(). Voici le code complet :
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
        printTemperature()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Exécutez le programme. La sortie doit se présenter comme suit :
Weather forecast
Sunny
30°C

Dans ce code, la coroutine est d'abord suspendue avec le délai de la fonction de suspension printForecast(), puis reprend après ce délai d'une seconde. Le texte Sunny s'affiche dans la sortie. La fonction printForecast() est renvoyée à l'appelant.

Ensuite, la fonction printTemperature() est appelée. Cette coroutine est suspendue lorsqu'elle atteint l'appel delay(), puis reprend une seconde plus tard et termine d'afficher la valeur de température sur la sortie. La fonction printTemperature() a terminé toutes les tâches et est renvoyée.

Dans le corps runBlocking(), il n'y a plus aucune tâche à exécuter. La fonction runBlocking() est donc renvoyée et le programme se termine.

Comme indiqué précédemment, runBlocking() est synchrone et chaque appel dans le corps est appelé de manière séquentielle. Notez qu'une fonction de suspension bien conçue n'est renvoyée qu'une fois tout le travail terminé. Ces fonctions de suspension s'exécutent donc l'une après l'autre.

  1. (Facultatif) Si vous souhaitez savoir combien de temps il faut pour exécuter ce programme avec les délais, vous pouvez encapsuler votre code dans un appel à measureTimeMillis(). Celui-ci renvoie le temps nécessaire en millisecondes pour exécuter le bloc de code transmis. Ajoutez l'instruction d'importation (import kotlin.system.*) pour accéder à cette fonction. Affichez la durée d'exécution et divisez-la par 1000.0 pour convertir les millisecondes en secondes.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            printForecast()
            printTemperature()
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}
suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}

Sortie :

Weather forecast
Sunny
30°C
Execution time: 2.128 seconds

La sortie indique que l'exécution a pris environ 2,1 secondes. (La durée d'exécution précise peut être légèrement différente pour vous.) Cela semble raisonnable, car chacune des fonctions de suspension est retardée d'une seconde.

Jusqu'à présent, vous avez vu que le code d'une coroutine est appelé par défaut de manière séquentielle. Vous devez être explicite si vous souhaitez que les éléments s'exécutent simultanément. Vous découvrirez comment procéder dans la section suivante. Vous utiliserez la boucle d'événements coopérative pour effectuer plusieurs tâches en même temps, ce qui accélérera la durée d'exécution du programme.

3. Code asynchrone

launch()

Utilisez la fonction launch() de la bibliothèque de coroutines pour lancer une nouvelle coroutine. Pour exécuter des tâches simultanément, ajoutez plusieurs fonctions launch() à votre code afin que plusieurs coroutines puissent être en cours en même temps.

Dans Kotlin, les coroutines suivent un concept clé appelé simultanéité structurée : votre code est séquentiel par défaut et coopère avec une boucle d'événements sous-jacente, sauf si vous demandez explicitement une exécution simultanée (par exemple, en utilisant launch()). Nous partons du principe que si vous appelez une fonction, celle-ci doit se terminer complètement au moment de son renvoi, quel que soit le nombre de coroutines qu'elle a pu utiliser dans ses détails d'implémentation. Même en cas d'échec générant une exception, une fois que l'exception est levée, il n'y a plus de tâches en attente dans la fonction. Par conséquent, tout le travail est terminé une fois que le flux de contrôle est renvoyé par la fonction, qu'elle ait généré une exception ou qu'elle ait mené à bien son exécution.

  1. Commencez avec le code des étapes précédentes. Utilisez la fonction launch() pour déplacer chaque appel vers printForecast() et printTemperature() respectivement dans leurs propres coroutines.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Exécutez le programme. Voici la sortie :
Weather forecast
Sunny
30°C

La sortie est la même, mais vous avez peut-être remarqué qu'il est plus rapide d'exécuter le programme. Auparavant, vous deviez attendre que la fonction de suspension printForecast() soit complètement terminée avant de passer à la fonction printTemperature(). printForecast() et printTemperature() peuvent désormais s'exécuter simultanément, car ils se trouvent dans des coroutines distinctes.

L'instruction println (prévision météo) se trouve dans un rectangle en haut du diagramme. En dessous se trouve une flèche verticale pointant vers le bas. À droite de cette flèche verticale, une ramification se dirige vers la droite et une flèche pointe vers un rectangle contenant l'instruction printForecast(). À droite de cette flèche verticale d'origine se trouve une autre ramification à droite, dont la flèche pointe vers un rectangle contenant l'instruction printTemperature().

L'appel à launch { printForecast() } peut être renvoyé avant que tout le travail dans printForecast() ne soit terminé. C'est toute la beauté des coroutines. Vous pouvez passer à l'appel launch() suivant pour démarrer la coroutine suivante. De même, launch { printTemperature() } est également renvoyé avant même que tout le travail ne soit terminé.

  1. (Facultatif) Si vous souhaitez connaître la vitesse actuelle du programme, vous pouvez ajouter le code measureTimeMillis() pour voir la durée d'exécution.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            launch {
                printForecast()
            }
            launch {
                printTemperature()
            }
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}

...

Sortie :

Weather forecast
Sunny
30°C
Execution time: 1.122 seconds

Comme vous pouvez le constater, la durée d'exécution est passée d'environ 2,1 secondes à 1,1 seconde. Il est donc plus rapide d'exécuter le programme une fois que vous ajoutez des opérations simultanées. Vous pouvez supprimer ce code de mesure du temps avant de passer aux étapes suivantes.

Selon vous, que se passe-t-il si vous ajoutez une autre instruction d'affichage après le deuxième appel launch(), avant la fin du code runBlocking() ? Où ce message apparaîtra-t-il dans la sortie ?

  1. Modifiez le code runBlocking() pour ajouter une instruction d'affichage supplémentaire avant la fin de ce bloc.
...

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
        println("Have a good day!")
    }
}

...
  1. Exécutez le programme. Voici la sortie :
Weather forecast
Have a good day!
Sunny
30°C

À partir de cette sortie, vous pouvez constater qu'après le lancement des deux nouvelles coroutines pour printForecast() et printTemperature(), vous pouvez passer à l'instruction suivante, qui affiche Have a good day!. Cela démontre le côté "déclenchez et oubliez" de launch(). Vous déclenchez une nouvelle coroutine avec launch() et vous n'avez pas à vous soucier de la fin de son travail.

Ensuite, les coroutines finiront leur travail et afficheront les instructions de sortie restantes. Une fois que toutes les tâches (y compris toutes les coroutines) dans le corps de l'appel runBlocking() ont été effectuées, runBlocking() est renvoyé et le programme se termine.

Vous venez de modifier votre code synchrone en code asynchrone. Lorsqu'une fonction asynchrone est renvoyée, il est possible que la tâche ne soit pas encore terminée. Voici ce que vous avez vu dans le cas de launch(). La fonction a été renvoyée, mais son travail n'était pas encore terminé. En utilisant launch(), vous pouvez exécuter plusieurs tâches simultanément dans votre code, ce qui est une fonctionnalité performante à utiliser dans les applications Android que vous développez.

async()

Dans le monde réel, vous ne saurez pas combien de temps prendront les requêtes réseau pour les prévisions et la température. Si vous souhaitez afficher un bulletin météo unifié lorsque les deux tâches sont terminées, l'approche actuelle avec launch() n'est pas suffisante. C'est là que async() entre en jeu.

Utilisez la fonction async() de la bibliothèque de coroutines si vous vous souciez du moment où la coroutine se termine et avez besoin d'une valeur renvoyée.

La fonction async() renvoie un objet de type Deferred, qui est comme une promesse que le résultat sera là quand il sera prêt. Vous pouvez accéder au résultat de l'objet Deferred à l'aide de await().

  1. Modifiez d'abord vos fonctions de suspension pour qu'elles renvoient String au lieu d'afficher les données de prévision et de température. Remplacez les noms des fonctions printForecast() et printTemperature() par getForecast() et getTemperature().
...

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Modifiez votre code runBlocking() de sorte qu'il utilise async() au lieu de launch() pour les deux coroutines. Stockez la valeur renvoyée de chaque appel async() dans des variables appelées forecast et temperature, qui sont des objets Deferred contenant un résultat de type String. (La spécification du type est facultative, car l'inférence de type est activée dans Kotlin. Elle est incluse ci-dessous pour vous permettre de voir plus clairement ce qui est renvoyé par les appels async().)
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        ...
    }
}

...
  1. Plus tard dans la coroutine, après les deux appels async(), vous pourrez accéder au résultat de ces coroutines en appelant await() sur les objets Deferred. Dans ce cas, vous pouvez afficher la valeur de chaque coroutine à l'aide de forecast.await() et de temperature.await().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        println("${forecast.await()} ${temperature.await()}")
        println("Have a good day!")
    }
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Exécutez le programme. La sortie sera comme suit :
Weather forecast
Sunny 30°C
Have a good day!

Excellent ! Vous avez créé deux coroutines exécutées simultanément pour obtenir les données de prévision et de température. Lorsqu'elles ont terminé, elles ont renvoyé une valeur. Vous avez ensuite combiné les deux valeurs renvoyées dans une seule instruction d'affichage : Sunny 30°C.

Décomposition en parallèle

Nous pouvons pousser cet exemple météo un peu plus loin et voir comment les coroutines peuvent être utiles pour décomposer le travail en parallèle. La décomposition en parallèle implique de prendre un problème et de le décomposer en tâches secondaires plus petites pouvant être résolues en parallèle. Lorsque les résultats des tâches secondaires sont prêts, vous pouvez les combiner en un résultat final.

Dans votre code, extrayez la logique du bulletin météo du corps de runBlocking() dans une seule fonction getWeatherReport() qui renvoie la chaîne combinée de Sunny 30°C.

  1. Définissez une nouvelle fonction de suspension getWeatherReport() dans votre code.
  2. Définissez la fonction sur le résultat d'un appel de la fonction coroutineScope{} avec un bloc lambda vide qui contiendra à terme la logique permettant d'obtenir le bulletin météo.
...

suspend fun getWeatherReport() = coroutineScope {

}

...

coroutineScope{} crée un champ d'application local pour cette tâche de bulletin météo. Les coroutines lancées dans ce champ d'application sont regroupées dans ce champ d'application, ce qui a des conséquences sur l'annulation et les exceptions, que vous découvrirez bientôt.

  1. Dans le corps de coroutineScope(), créez deux coroutines à l'aide de async() pour extraire les données de prévision et de température, respectivement. Créez la chaîne de bulletin météo en combinant ces résultats des deux coroutines. Pour ce faire, appelez await() sur chacun des objets Deferred renvoyés par les appels async(). Cela garantit que chaque coroutine termine son travail et renvoie son résultat, avant que nous ne revenions de cette fonction.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

...
  1. Appelez cette nouvelle fonction getWeatherReport() à partir de runBlocking(). Voici le code complet :
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Exécutez le programme. Voici la sortie :
Weather forecast
Sunny 30°C
Have a good day!

La sortie est la même, mais il y a des points importants à retenir. Comme indiqué précédemment, coroutineScope() n'est renvoyé qu'une fois que toutes ses tâches, y compris les coroutines qu'il a lancées, sont terminées. Dans ce cas, les coroutines getForecast() et getTemperature() doivent terminer et renvoyer leurs résultats respectifs. Ensuite, le texte Sunny et 30°C sont combinés et renvoyés par le champ d'application. Le bulletin météo Sunny 30°C est affiché sur la sortie, et l'appelant peut passer à la dernière instruction d'affichage de Have a good day!.

Avec coroutineScope(), même si la fonction est exécutée en interne simultanément, elle apparaît à l'appelant comme une opération synchrone, car coroutineScope n'est renvoyé que lorsque tout le travail est terminé.

L'avantage principal de la simultanéité structurée est que vous pouvez effectuer plusieurs opérations simultanées et les regrouper en une seule opération synchrone, où la simultanéité est un détail d'implémentation. La seule condition requise pour le code appelant est d'être dans une fonction de suspension ou une coroutine. En dehors de cela, la structure du code appelant n'a pas besoin de prendre en compte les détails de simultanéité.

4. Exceptions et annulation

Abordons maintenant les cas d'erreurs ou d'annulation de tâches.

Présentation des exceptions

Une exception est un événement inattendu qui se produit pendant l'exécution de votre code. Vous devez mettre en place des méthodes appropriées pour gérer ces exceptions afin d'éviter que votre application ne plante et ne nuise à l'expérience utilisateur.

Voici un exemple de programme qui s'arrête prématurément avec une exception. L'objectif de ce programme est de calculer le nombre de pizzas que chaque personne pourra manger en divisant numberOfPizzas / numberOfPeople. Supposons que vous oubliez d'affecter une valeur réelle à numberOfPeople.

fun main() {
    val numberOfPeople = 0
    val numberOfPizzas = 20
    println("Slices per person: ${numberOfPizzas / numberOfPeople}")
}

Lorsque vous exécuterez le programme, il plantera avec une exception arithmétique, car on ne peut pas diviser un nombre par zéro.

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at FileKt.main (File.kt:4)
 at FileKt.main (File.kt:-1)
 at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (:-2)

Ce problème a une solution simple : vous pouvez remplacer la valeur initiale de numberOfPeople par une valeur non nulle. Cependant, à mesure que votre code gagne en complexité, il arrive que vous ne puissiez pas anticiper et empêcher toutes les exceptions qui se produisent.

Que se passe-t-il lorsqu'une de vos coroutines échoue avec une exception ? Modifiez le code du programme météo pour le découvrir.

Exceptions avec les coroutines

  1. Commencez par le programme météo de la section précédente.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

Dans l'une des fonctions de suspension, générez intentionnellement une exception pour voir l'effet. L'apparition d'une erreur inattendue lors de la récupération des données sur le serveur est simulée, ce qui est plausible.

  1. Dans la fonction getTemperature(), ajoutez une ligne de code qui générera une exception. Écrivez une expression "throw" à l'aide du mot clé throw en Kotlin, suivi d'une nouvelle instance d'exception qui s'étend à partir de Throwable.

Par exemple, vous pouvez envoyer une AssertionError et transmettre une chaîne de message décrivant l'erreur plus en détail : throw AssertionError("Temperature is invalid"). En renvoyant cette exception, vous arrêtez l'exécution de la fonction getTemperature().

...

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}

Vous pouvez également remplacer le délai par 500 millisecondes pour la méthode getTemperature(), afin de savoir que l'exception se produira avant que l'autre fonction getForecast() puisse terminer son travail.

  1. Exécutez le programme pour voir le résultat.
Weather forecast
Exception in thread "main" java.lang.AssertionError: Temperature is invalid
 at FileKt.getTemperature (File.kt:24)
 at FileKt$getTemperature$1.invokeSuspend (File.kt:-1)
 at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)

Pour comprendre ce comportement, sachez qu'il existe une relation parent-enfant entre les coroutines. Vous pouvez lancer une coroutine (enfant) à partir d'une autre coroutine (parent). À mesure que vous en lancez d'autres à partir de ces coroutines, vous pouvez créer une hiérarchie complète de coroutines.

La coroutine qui exécute getTemperature() et la coroutine qui exécute getForecast() sont des coroutines enfants de la même coroutine parent. Le comportement que vous observez avec les exceptions dans les coroutines est dû à la simultanéité structurée. Lorsque l'une des coroutines enfants échoue avec une exception, elle est propagée vers le haut. La coroutine parent est annulée, ce qui annule à son tour toutes les autres coroutines enfants (par exemple, la coroutine qui exécute getForecast() dans ce cas). Enfin, l'erreur se propage vers le haut, et le programme plante avec l'AssertionError.

Exceptions try-catch

Si vous savez que certaines parties de votre code peuvent éventuellement générer une exception, vous pouvez entourer ce code d'un bloc try-catch. Vous pouvez repérer l'exception et la gérer plus facilement dans votre application, par exemple en présentant un message d'erreur descriptif à l'utilisateur. Voici un extrait de code qui illustre ce cas de figure :

try {
    // Some code that may throw an exception
} catch (e: IllegalArgumentException) {
    // Handle exception
}

Cette approche fonctionne également pour le code asynchrone avec les coroutines. Vous pouvez toujours utiliser une expression try-catch pour intercepter et gérer les exceptions dans les coroutines. En effet, avec la simultanéité structurée, le code séquentiel reste synchrone, de sorte que le bloc try-catch continuera de fonctionner de la même manière.

...

fun main() {
    runBlocking {
        ...
        try {
            ...
            throw IllegalArgumentException("No city selected")
            ...
        } catch (e: IllegalArgumentException) {
            println("Caught exception $e")
            // Handle error
        }
    }
}

...

Pour vous familiariser avec la gestion des exceptions, modifiez le programme météo afin de détecter l'exception que vous avez ajoutée précédemment et imprimez-la dans la sortie.

  1. Dans la fonction runBlocking(), ajoutez un bloc try-catch autour du code qui appelle getWeatherReport(). Imprimez l'erreur détectée et affichez un message indiquant que le bulletin météo n'est pas disponible.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        try {
            println(getWeatherReport())
        } catch (e: AssertionError) {
            println("Caught exception in runBlocking(): $e")
            println("Report unavailable at this time")
        }
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Exécutez le programme. L'erreur est maintenant gérée correctement, et l'exécution du programme peut être menée à bien.
Weather forecast
Caught exception in runBlocking(): java.lang.AssertionError: Temperature is invalid
Report unavailable at this time
Have a good day!

À partir de la sortie, vous pouvez constater que getTemperature() génère une exception. Dans le corps de la fonction runBlocking(), entourez l'appel println(getWeatherReport()) dans un bloc try-catch. Interceptez le type d'exception attendu (AssertionError dans cet exemple). Imprimez ensuite l'exception dans la sortie sous la forme "Caught exception", suivie de la chaîne comprenant le message d'erreur. Pour gérer l'erreur, informez l'utilisateur que le bulletin météo n'est pas disponible avec une instruction println() supplémentaire : Report unavailable at this time.

Notez que ce comportement signifie qu'en cas d'échec d'obtention de la température, il n'y aura aucun bulletin météo (même si une prévision valide a été récupérée).

Selon le comportement souhaité pour votre programme, il existe une autre manière de gérer l'exception dans le programme météo.

  1. Déplacez la gestion des erreurs de sorte que le comportement try-catch se produise réellement dans la coroutine lancée par async() pour récupérer la température. Le bulletin météo pourra ainsi imprimer la prévision, même en cas d'échec de l'obtention de la température. Voici le code :
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Exécutez le programme.
Weather forecast
Caught exception java.lang.AssertionError: Temperature is invalid
Sunny { No temperature found }
Have a good day!

Dans la sortie, vous pouvez constater que l'appel de getTemperature() a échoué avec une exception, mais que le code dans async() a pu intercepter cette exception et la gérer correctement en demandant à la coroutine de renvoyer une String qui indique que la température est introuvable. Le bulletin météo peut tout de même être imprimé, avec la prévision Sunny. La température n'est pas indiquée dans le bulletin météo, mais un message s'affiche pour expliquer que la température est introuvable. Cette expérience utilisateur est préférable à celle où le programme plante avec l'erreur.

Considérez cette approche de gestion des erreurs comme si async() était le producteur lorsqu'une coroutine est démarrée avec lui. await() est le consommateur, car il attend d'utiliser le résultat de la coroutine. Le producteur effectue le travail et génère un résultat. Le consommateur utilise ce résultat. S'il existe une exception dans le producteur, le consommateur la recevra si elle n'est pas gérée, et la coroutine échouera. Toutefois, si le producteur est en mesure d'intercepter et de gérer l'exception, le consommateur ne la verra pas et recevra un résultat valide.

Pour rappel, voici le code getWeatherReport() :

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

Dans ce cas, le producteur (async()) a pu intercepter et gérer l'exception, mais a quand même renvoyé un résultat sous forme de String : "{ No temperature found }". Le consommateur (await()) reçoit ce résultat String et n'a même pas besoin de savoir qu'une exception s'est produite. Il s'agit d'une autre option permettant de gérer de façon optimale une exception qui pourrait affecter votre code.

Vous savez maintenant que les exceptions se propagent vers le haut dans l'arborescence des coroutines, à moins qu'elles ne soient traitées. Il est également important d'être attentif lorsque l'exception se propage jusqu'à la racine de la hiérarchie, car cela peut entraîner le plantage de l'ensemble de l'application. Pour en savoir plus sur la gestion des exceptions, consultez l'article de blog Exceptions dans les coroutines et l'article Gestion des exceptions de coroutine.

Annulation

L'annulation des coroutines est semblable aux exceptions. Ce scénario est généralement géré par l'utilisateur lorsqu'un événement a entraîné l'annulation par l'application du travail qu'elle avait commencé.

Par exemple, supposons que l'utilisateur ait sélectionné une préférence dans l'application et qu'il ne souhaite plus y voir les valeurs de température. Il veut seulement connaître les prévisions météo (par exemple, Sunny), mais pas la température exacte. Dans ce cas, annulez la coroutine qui reçoit actuellement les données de température.

  1. Commencez par le code initial ci-dessous (sans annulation).
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Après un certain temps, annulez la coroutine qui extrait les informations de température, afin que le bulletin météo n'affiche que la prévision. Modifiez la valeur de retour du bloc coroutineScope pour qu'elle corresponde uniquement à la chaîne de prévision météo.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }

    delay(200)
    temperature.cancel()

    "${forecast.await()}"
}

...
  1. Exécutez le programme. Voici la sortie : Le bulletin météo ne contient que la prévision météo Sunny, mais pas la température, car cette coroutine a été annulée.
Weather forecast
Sunny
Have a good day!

Ce que vous avez appris ici est qu'une coroutine peut être annulée, mais elle n'affecte pas les autres coroutines du même champ d'application. De même, la coroutine parent n'est pas annulée.

Dans cette section, vous avez vu comment l'annulation et les exceptions se comportent dans les coroutines, et comment cette approche est liée à la hiérarchie des coroutines. Intéressons-nous de plus près aux concepts formels sur lesquels reposent les coroutines afin que vous puissiez comprendre tous les tenants et les aboutissants.

5. Concepts de coroutine

Lorsque vous exécutez un travail de manière asynchrone ou simultanée, vous devez répondre à des questions sur son exécution, la durée d'existence de la coroutine, ce qui doit se produire s'il est annulé ou échoue en raison d'une erreur, etc. Les coroutines suivent le principe de la simultanéité structurée, qui vous oblige à répondre à ces questions lorsque vous utilisez des coroutines dans votre code par le biais d'une combinaison de mécanismes.

Tâche

Lorsque vous lancez une coroutine avec la fonction launch(), elle renvoie une instance de Job. La tâche contient un handle, aussi appelé "référence", à la coroutine afin que vous puissiez gérer son cycle de vie.

val job = launch { ... }

La tâche peut être utilisée pour contrôler le cycle de vie ou la durée de vie de la coroutine, par exemple pour annuler la coroutine si vous n'avez plus besoin de la tâche.

job.cancel()

Une tâche vous permet de vérifier si elle est active, annulée ou terminée. La tâche est terminée si la coroutine et toutes les coroutines qu'elle a lancées ont terminé l'ensemble de leur travail. Notez que la coroutine s'est peut-être terminée pour une autre raison (par exemple, annulation ou échec avec une exception), mais la tâche est toujours considérée comme terminée à ce stade.

Les tâches assurent également le suivi de la relation parent-enfant entre les coroutines.

Hiérarchie des tâches

Lorsqu'une coroutine lance une autre coroutine, la tâche renvoyée par la nouvelle coroutine est appelée l'enfant de la tâche parente d'origine.

val job = launch {
    ...

    val childJob = launch { ... }

    ...
}

Ces relations parent-enfant forment une hiérarchie des tâches, dans laquelle chaque tâche peut lancer des tâches, etc.

Ce schéma présente une arborescence de tâches. À la racine de la hiérarchie se trouve une tâche parente. Elle a trois enfants appelés : Tâche 1 enfant, Tâche 2 enfant et Tâche 3 enfant. Tâche 1 enfant a deux enfants : Tâche 1a enfant et Tâche 1b enfant. Tâche 2 enfant a un seul enfant appelé Tâche 2a enfant. Enfin, Tâche 3 enfant a deux enfants : Tâche 3a enfant et Tâche 3b enfant.

Cette relation parent-enfant est importante, car elle dicte un certain comportement à l'enfant et au parent, ainsi qu'aux autres enfants appartenant au même parent. Ce comportement a été observé dans les exemples précédents avec le programme météo.

  • Si une tâche parente est annulée, ses tâches enfants le sont également.
  • Lorsqu'une tâche enfant est annulée à l'aide de job.cancel(), elle se termine, mais pas son parent.
  • Si une tâche échoue avec une exception, elle annule son parent avec cette exception. On parle alors de propagation de l'erreur vers le haut (vers le parent, le parent du parent, etc.). .

CoroutineScope

Les coroutines sont généralement lancées dans un CoroutineScope. Cela garantit qu'il n'y a pas de coroutines non gérées et perdues, ce qui pourrait gaspiller des ressources.

launch() et async() sont des fonctions d'extension sur CoroutineScope. Appelez launch() ou async() sur le champ d'application pour créer une coroutine dans ce champ d'application.

Un CoroutineScope est lié à un cycle de vie, qui définit les limites de durée des coroutines dans ce champ d'application. Si un champ d'application est annulé, sa tâche est annulée, et l'annulation de cette tâche est appliquée à ses tâches enfants. Si une tâche enfant du champ d'application échoue avec une exception, les autres tâches enfants sont annulées, la tâche parente est annulée et l'exception est renvoyée à l'appelant.

CoroutineScope dans Kotlin Playground

Dans cet atelier de programmation, vous avez utilisé runBlocking(), qui fournit un CoroutineScope pour votre programme. Vous avez également appris à utiliser coroutineScope { } pour créer un champ d'application dans la fonction getWeatherReport().

CoroutineScope dans les applications Android

Android offre une compatibilité avec les champs d'application des coroutines dans les entités dont le cycle de vie est bien défini, comme Activity (lifecycleScope) et ViewModel (viewModelScope). Les coroutines démarrées dans ces champs d'application respecteront le cycle de vie de l'entité correspondante, par exemple Activity ou ViewModel.

Par exemple, supposons que vous démarriez une coroutine dans une Activity avec le champ d'application de coroutine fourni, lifecycleScope. Si l'activité est détruite, le lifecycleScope sera annulé et toutes ses coroutines enfants seront automatiquement annulées. Il vous suffit de décider si vous souhaitez que la coroutine suive le cycle de vie de l'Activity.

Dans l'application Android RaceTracker sur laquelle vous allez travailler, vous allez apprendre à limiter vos coroutines au cycle de vie d'un composable.

Détails d'implémentation de CoroutineScope

Si vous vérifiez le code source pour savoir comment CoroutineScope.kt est implémenté dans la bibliothèque de coroutines Kotlin, vous pouvez constater que CoroutineScope est déclaré en tant qu'interface et qu'il contient un CoroutineContext en tant que variable.

Les fonctions launch() et async() créent une coroutine enfant dans ce champ d'application. L'enfant hérite également du contexte du champ d'application. Que contient le contexte ? Voyons cela plus en détail.

CoroutineContext

CoroutineContext fournit des informations sur le contexte dans lequel la coroutine sera exécutée. CoroutineContext est essentiellement une carte qui stocke des éléments, dans laquelle chaque élément a une clé unique. Ces champs ne sont pas obligatoires, mais voici quelques exemples de ce que peut contenir un contexte :

  • nom : nom de la coroutine pour l'identifier de manière unique.
  • tâche : contrôle le cycle de vie de la coroutine.
  • répartiteur : distribue les tâches au thread approprié.
  • gestionnaire d'exceptions : gère les exceptions générées par le code exécuté dans la coroutine.

Chacun des éléments d'un contexte peut être ajouté avec l'opérateur +. Par exemple, un CoroutineContext peut être défini comme suit :

Job() + Dispatchers.Main + exceptionHandler

Comme aucun nom n'est indiqué, le nom de coroutine par défaut est utilisé.

Dans une coroutine, si vous lancez une nouvelle coroutine, la coroutine enfant hérite du CoroutineContext de la coroutine parente, mais remplace la tâche spécifiquement pour la coroutine que vous venez de créer. Vous pouvez également remplacer tous les éléments hérités du contexte parent en transmettant des arguments aux fonctions launch() ou async() pour les parties du contexte que vous souhaitez différencier.

scope.launch(Dispatchers.Default) {
    ...
}

Pour en savoir plus sur CoroutineContext et sur la façon dont le contexte est hérité du parent, regardez cette conférence vidéo KotlinConf.

Vous avez vu plusieurs fois la mention de répartiteur. Son rôle est de distribuer ou d'attribuer le travail à un thread. Parlons plus en détail des threads et des répartiteurs.

Répartiteur

Les coroutines utilisent les répartiteurs pour déterminer le thread à utiliser pour son exécution. Un thread peut être démarré, effectuer des tâches (exécuter du code), puis se terminer lorsqu'il n'y a plus de tâche à effectuer.

Lorsqu'un utilisateur démarre votre application, le système Android crée un processus et un thread unique pour l'exécution de votre application, appelé thread principal. Le thread principal gère de nombreuses opérations importantes pour votre application, y compris les événements système Android, le dessin de l'UI à l'écran, la gestion des événements d'entrée utilisateur, etc. Par conséquent, la plupart du code que vous écrivez pour votre application s'exécutera probablement sur le thread principal.

Il existe deux termes clés en ce qui concerne le comportement des threads de votre code : le blocage et le non-blocage. Une fonction standard bloque le thread appelant jusqu'à ce que son travail soit terminé. Autrement dit, elle ne génère pas le thread appelant tant que le travail n'est pas terminé. Aucune autre tâche ne peut être effectuée en attendant. À l'inverse, le code non bloquant génère le thread appelant jusqu'à ce qu'une certaine condition soit remplie. Vous pouvez donc effectuer d'autres tâches en attendant. Vous pouvez utiliser une fonction asynchrone pour effectuer des tâches non bloquantes, car celle-ci est renvoyée avant que le travail soit terminé.

Dans le cas des applications Android, vous ne devez appeler le code bloquant dans le thread principal que s'il s'exécute assez rapidement. L'objectif est de maintenir le thread principal débloqué afin qu'il puisse exécuter le travail immédiatement si un nouvel événement est déclenché. Ce thread principal est le thread UI de vos activités. Il est responsable du dessin de l'UI et des événements liés à l'UI. En cas de modification à l'écran, l'UI doit être redessinée. Dans le cas d'une animation à l'écran, par exemple, l'UI doit être redessinée fréquemment pour que la transition semble fluide. Si le thread principal doit exécuter un bloc de travail de longue durée, l'écran ne sera pas mis à jour aussi souvent et l'utilisateur verra une transition soudaine ("à-coup") ou l'application peut se figer ou mettre du temps à répondre.

Nous devons donc retirer tous les éléments de travail de longue durée du thread principal et les gérer dans un autre thread. Votre application démarre avec un seul thread principal, mais vous pouvez choisir d'en créer plusieurs pour effectuer des tâches supplémentaires. Ces threads supplémentaires peuvent être appelés threads de nœud de calcul. Il est parfaitement acceptable pour une tâche de longue durée de bloquer un thread de travail pendant une longue période, car en attendant, le thread principal est débloqué et peut répondre activement à l'utilisateur.

Kotlin fournit les répartiteurs intégrés suivants :

  • Dispatchers.Main : utilisez ce répartiteur pour exécuter une coroutine sur le thread Android principal. Ce répartiteur est principalement utilisé pour gérer les mises à jour et les interactions de l'UI et effectuer un travail rapide.
  • Dispatchers.IO : ce répartiteur est optimisé pour exécuter les E/S de disque ou de réseau en dehors du thread principal. Par exemple, lire ou écrire dans des fichiers, et exécuter des opérations réseau.
  • Dispatchers.Default : répartiteur par défaut utilisé lorsque vous appelez launch() et async(), lorsqu'aucun répartiteur n'est spécifié dans son contexte. Vous pouvez utiliser ce répartiteur pour effectuer des tâches nécessitant beaucoup de ressources de calcul en dehors du thread principal. Par exemple, traitement d'un fichier image bitmap.

Essayez l'exemple suivant dans Kotlin Playground pour mieux comprendre les répartiteurs de coroutines.

  1. Remplacez tout code présent dans Kotlin Playground par le code suivant :
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        launch {
            delay(1000)
            println("10 results found.")
        }
        println("Loading...")
    }
}
  1. Encapsulez maintenant le contenu de la coroutine lancée avec un appel à withContext() pour modifier le CoroutineContext dans lequel la coroutine est exécutée, et remplacez spécifiquement le répartiteur. Utilisez Dispatchers.Default (au lieu de Dispatchers.Main, qui est actuellement utilisé pour le reste du code de coroutine du programme).
...

fun main() {
    runBlocking {
        launch {
            withContext(Dispatchers.Default) {
                delay(1000)
                println("10 results found.")
            }
        }
        println("Loading...")
    }
}

Il est possible de changer de répartiteur, car withContext() est lui-même une fonction de suspension. Il exécute le bloc de code fourni à l'aide d'un nouveau CoroutineContext. Le nouveau contexte provient du contexte de la tâche parente (le bloc launch() externe), sauf qu'il remplace le répartiteur utilisé dans le contexte parent par celui spécifié ici : Dispatchers.Default. C'est ainsi que nous pouvons passer de l'exécution du travail avec Dispatchers.Main à l'utilisation de Dispatchers.Default.

  1. Exécutez le programme. La sortie doit se présenter comme suit :
Loading...
10 results found.
  1. Ajoutez des instructions d'affichage pour voir le thread que vous utilisez en appelant Thread.currentThread().name.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("${Thread.currentThread().name} - runBlocking function")
                launch {
            println("${Thread.currentThread().name} - launch function")
            withContext(Dispatchers.Default) {
                println("${Thread.currentThread().name} - withContext function")
                delay(1000)
                println("10 results found.")
            }
            println("${Thread.currentThread().name} - end of launch function")
        }
        println("Loading...")
    }
}
  1. Exécutez le programme. La sortie doit se présenter comme suit :
main @coroutine#1 - runBlocking function
Loading...
main @coroutine#2 - launch function
DefaultDispatcher-worker-1 @coroutine#2 - withContext function
10 results found.
main @coroutine#2 - end of launch function

À partir de cette sortie, vous pouvez constater que la majeure partie du code est exécutée dans des coroutines sur le thread principal. Cependant, la partie du code dans le bloc withContext(Dispatchers.Default) est exécutée dans une coroutine sur un thread de travail Répartiteur par défaut (qui n'est pas le thread principal). Notez qu'après le renvoi de withContext(), la coroutine revient à l'exécution sur le thread principal (comme en témoigne l'instruction de sortie main @coroutine#2 - end of launch function). Cet exemple montre que vous pouvez changer de répartiteur en modifiant le contexte utilisé pour la coroutine.

Si vous avez démarré des coroutines sur le thread principal et que vous souhaitez déplacer certaines opérations hors du thread principal, vous pouvez utiliser withContext pour changer le répartiteur utilisé pour ce travail. Choisissez l'un des répartiteurs disponibles : Main, Default et IO selon le type d'opération. Cette tâche peut ensuite être attribuée à un thread (ou à un groupe de threads appelé pool de threads) conçu à cet effet. Les coroutines peuvent se suspendre d'elles-mêmes, et le répartiteur influence également la manière dont elles sont réactivées.

Notez que, lorsque vous travaillez avec des bibliothèques populaires comme Room et Retrofit (dans ce module et le suivant), vous n'aurez peut-être pas besoin de modifier explicitement le répartiteur vous-même si le code de la bibliothèque gère déjà cette tâche à l'aide d'un autre répartiteur de coroutine tel que Dispatchers.IO.. Dans ce cas, les fonctions suspend révélées par ces bibliothèques peuvent déjà être sécurisées et peuvent être appelées à partir d'une coroutine exécutée sur le thread principal. La bibliothèque gère le basculement du répartiteur vers un autre qui utilise des threads de nœud de calcul.

Vous disposez désormais d'une vue d'ensemble des aspects importants des coroutines, ainsi que du rôle que jouent CoroutineScope, CoroutineContext, CoroutineDispatcher et Jobs dans la conception du cycle de vie et du comportement d'une coroutine.

6. Conclusion

Vous voici à la conclusion de cet atelier de programmation complexe sur les coroutines. Vous avez appris que les coroutines sont très utiles, car leur exécution peut être suspendue, libérant ainsi le thread sous-jacent pour effectuer d'autres tâches. Les coroutines peuvent ensuite être réactivées. Cela vous permet d'exécuter des opérations simultanées dans votre code.

Dans Kotlin, le code de coroutine suit le principe de la simultanéité structurée. Elle est séquentielle par défaut. Vous devez donc être explicite si vous souhaitez utiliser la simultanéité (par exemple, en utilisant launch() ou async()). Avec la simultanéité structurée, vous pouvez effectuer plusieurs opérations simultanées et les regrouper en une seule opération synchrone, où la simultanéité est un détail d'implémentation. La seule condition requise pour le code appelant est d'être dans une fonction de suspension ou une coroutine. En dehors de cela, la structure du code appelant n'a pas besoin de prendre en compte les détails de simultanéité. Cela rend votre code asynchrone plus facile à lire et à comprendre.

La simultanéité structurée effectue le suivi de chacune des coroutines lancées dans votre application et garantit qu'elles ne sont pas perdues. Les coroutines peuvent avoir une hiérarchie : les tâches peuvent lancer des tâches secondaires, qui peuvent ensuite lancer des tâches secondaires. Les tâches maintiennent la relation parent-enfant entre les coroutines et vous permettent de contrôler le cycle de vie de la coroutine.

Le lancement, l'exécution, l'annulation et l'échec sont quatre opérations courantes dans l'exécution d'une coroutine. Pour faciliter la gestion des programmes simultanés, la simultanéité structurée définit les principes qui constituent la base de la gestion des opérations courantes de la hiérarchie :

  1. Lancement : lancez une coroutine dans un champ d'application ayant une limite définie sur sa durée de vie.
  2. Exécution : la tâche n'est pas terminée tant que ses tâches enfants ne sont pas terminées.
  3. Annulation : cette opération doit être propagée vers le bas. Lorsqu'une coroutine est annulée, les coroutines enfants doivent également être annulées.
  4. Échec : cette opération doit se propager vers le haut. Lorsqu'une coroutine génère une exception, le parent annule tous ses enfants, s'annule lui-même et propage l'exception à son parent. Le processus se poursuit jusqu'à ce que l'échec soit détecté et géré. Cela garantit que les erreurs dans le code sont correctement signalées et ne sont jamais perdues.

Grâce à des exercices pratiques sur les coroutines et la compréhension des concepts associés aux coroutines, vous êtes maintenant mieux à même d'écrire du code simultané dans votre application Android. En utilisant les coroutines pour la programmation asynchrone, votre code est plus facile à lire et à comprendre, plus robuste en cas d'annulations et d'exceptions, et offre une expérience plus optimale et réactive pour les utilisateurs finaux.

Résumé

  • Les coroutines vous permettent d'écrire du code de longue durée qui s'exécute simultanément sans apprendre un nouveau style de programmation. Les coroutines sont conçues pour une exécution séquentielle.
  • Les coroutines suivent le principe de la simultanéité structurée, qui garantit que le travail n'est pas perdu et lié à un champ d'application avec une certaine limite de durée de vie. Votre code est séquentiel par défaut et coopère avec une boucle d'événements sous-jacente, sauf si vous demandez explicitement une exécution simultanée (par exemple, en utilisant launch() ou async()). Nous partons du principe que si vous appelez une fonction, celle-ci doit se terminer complètement (sauf si elle échoue avec une exception) au moment de son renvoi, quel que soit le nombre de coroutines qu'elle a pu utiliser dans ses détails d'implémentation.
  • Le modificateur suspend permet de marquer une fonction dont l'exécution peut être suspendue et réactivée ultérieurement.
  • Une fonction suspend ne peut être appelée qu'à partir d'une autre fonction de suspension ou d'une coroutine.
  • Vous pouvez démarrer une nouvelle coroutine à l'aide des fonctions d'extension launch() ou async() sur CoroutineScope.
  • Les tâches jouent un rôle important pour garantir la simultanéité structurée en gérant le cycle de vie des coroutines et en maintenant la relation parent-enfant.
  • Un CoroutineScope permet de contrôler la durée de vie des coroutines par le biais de sa tâche et applique l'annulation et d'autres règles à ses enfants et à leurs enfants de manière récursive.
  • Un CoroutineContext définit le comportement d'une coroutine et peut inclure des références à un répartiteur de tâche et de coroutine.
  • Les coroutines utilisent un CoroutineDispatcher pour déterminer les threads à utiliser pour son exécution.

En savoir plus