Présentation des coroutines dans Android Studio

1. Avant de commencer

Dans l'atelier de programmation précédent, vous avez découvert les coroutines. Vous avez utilisé Kotlin Playground pour écrire du code simultané à l'aide de coroutines. Dans cet atelier de programmation, vous appliquerez vos connaissances sur les coroutines dans une application Android et son cycle de vie. Vous ajouterez du code pour lancer de nouvelles coroutines simultanément et découvrirez comment les tester.

Conditions préalables

  • Vous connaissez les bases de la programmation en Kotlin, y compris les fonctions et les lambdas.
  • Vous êtes capable de créer des mises en page dans Jetpack Compose.
  • Vous pouvez écrire des tests unitaires en Kotlin (consultez l'atelier de programmation Écrire des tests unitaires pour ViewModel).
  • Vous savez comment fonctionnent les threads et la simultanéité.
  • Vous disposez de connaissances de base sur les coroutines et CoroutineScope.

Objectifs de l'atelier

  • Créez une application de suivi de course, nommée Race Tracker, qui simule la progression d'une course entre deux joueurs. Considérez l'application comme une occasion d'expérimenter et d'en apprendre davantage sur les différents aspects des coroutines.

Points abordés

  • Utiliser des coroutines dans le cycle de vie d'une application Android
  • Principes de la simultanéité structurée
  • Écrire des tests unitaires pour tester les coroutines

Ce dont vous avez besoin

  • La dernière version stable d'Android Studio

2. Présentation de l'application

L'application Race Tracker simule une course entre deux joueurs. Son interface utilisateur est composée des boutons Start (Commencer)/Pause (Mettre en pause) et Reset (Réinitialiser), ainsi que de deux barres de progression indiquant la progression des coureurs. Les joueurs 1 et 2 sont programmés pour courir à des vitesses différentes. Au début de la course, le joueur 2 progresse deux fois plus vite que le joueur 1.

Vous utiliserez les coroutines dans l'application pour vous assurer que :

  • les deux joueurs font la course simultanément ;
  • l'interface utilisateur de l'application est responsive, et que les barres de progression s'incrémentent pendant la course.

Le code de démarrage contient le code d'interface utilisateur de l'application Race Tracker. L'objectif principal de cet atelier de programmation est de vous familiariser avec les coroutines Kotlin dans une application Android.

Obtenir le code de démarrage

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub du code :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-race-tracker.git
$ cd basic-android-kotlin-compose-training-race-tracker
$ git checkout starter

Vous pouvez parcourir le code de démarrage dans le dépôt GitHub de Race Tracker.

Tutoriel du code de démarrage

Vous pouvez lancer la course en cliquant sur le bouton Start (Démarrer). Pendant la course, le texte du bouton Start (Démarrer) devient Pause (Mettre en pause).

2ee492f277625f0a.png

Vous pouvez utiliser ce bouton à tout moment pour interrompre ou reprendre la course.

50e992f4cf6836b7.png

Au début de la course, un indicateur d'état affiche la progression de chaque joueur. La fonction modulable StatusIndicator affiche l'état de progression de chaque joueur. Il utilise le composable LinearProgressIndicator pour afficher la barre de progression. Vous utiliserez des coroutines pour mettre à jour la valeur de la progression.

79cf74d82eacae6f.png

RaceParticipant fournit les données de l'incrément de progression. Cette classe est un conteneur d'état pour chacun des joueurs et gère le name du coureur, le maxProgress à atteindre pour terminer la course, le délai entre chaque incrément, le currentProgress de la course et le initialProgress.

Dans la section suivante, vous utiliserez des coroutines pour implémenter la fonctionnalité permettant de simuler la progression de la course sans bloquer l'interface utilisateur de l'application.

3. Implémenter la progression de la course

Vous avez besoin de la fonction run() qui compare le currentProgress du joueur contre le maxProgress, qui reflète la progression totale de la course et utilise la fonction de suspension delay() pour ajouter un léger décalage entre les incréments de progression. Il s'agit d'une fonction suspend, car elle appelle une autre fonction de suspension delay(). Dans la suite de cet atelier de programmation, vous l'appellerez également à partir d'une coroutine. Pour implémenter la fonction, procédez comme suit :

  1. Ouvrez la classe RaceParticipant, qui est fournie dans le code de démarrage.
  2. Dans la classe RaceParticipant, définissez une nouvelle fonction suspend nommée run().
class RaceParticipant(
    ...
) {
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        
    }
    ...
}
  1. Pour simuler la progression de la course, ajoutez une boucle while qui s'exécute jusqu'à ce que currentProgress atteigne la valeur maxProgress, qui est définie sur 100.
class RaceParticipant(
    ...
    val maxProgress: Int = 100,
    ...
) {
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        while (currentProgress < maxProgress) {
            
        }
    }
    ...
}
  1. La valeur de currentProgress est définie sur initialProgress, soit 0. Pour simuler la progression du coureur, augmentez la valeur currentProgress de la valeur de la propriété progressIncrement dans la boucle "while". Notez que la valeur par défaut de progressIncrement est 1.
class RaceParticipant(
    ...
    val maxProgress: Int = 100,
    ...
    private val progressIncrement: Int = 1,
    private val initialProgress: Int = 0
) {
    ...
    var currentProgress by mutableStateOf(initialProgress)
        private set

    suspend fun run() {
        while (currentProgress < maxProgress) {
            currentProgress += progressIncrement
        }
    }
}
  1. Pour simuler différents intervalles de progression dans la course, utilisez la fonction de suspension delay(). Transmettez la valeur de la propriété progressDelayMillis en tant qu'argument.
suspend fun run() {
    while (currentProgress < maxProgress) {
        delay(progressDelayMillis)
        currentProgress += progressIncrement
    }
}

Si vous examinez le code que vous venez d'ajouter, vous verrez une icône à gauche de l'appel à la fonction delay() dans Android Studio, comme le montre la capture d'écran ci-dessous : 11b5df57dcb744dc.png.

Cette icône indique le point de suspension où la fonction peut être suspendue et reprise ultérieurement.

Le thread principal n'est pas bloqué pendant que la coroutine attend la fin du délai, comme illustré dans le schéma suivant :

a3c314fb082a9626.png

La coroutine suspend l'exécution après l'appel de la fonction delay() avec la valeur d'intervalle souhaitée, mais ne la bloque pas. Une fois le délai écoulé, la coroutine reprend l'exécution et met à jour la valeur de la propriété currentProgress.

4. Démarrer la course

Lorsque l'utilisateur appuie sur le bouton Start (Démarrer), vous devez démarrer la course en appelant la fonction de suspension run() pour chacune des deux instances de joueurs. Pour ce faire, vous devez lancer une coroutine pour appeler la fonction run().

Lorsque vous lancez une coroutine pour démarrer la course, vous devez vous assurer que les conditions suivantes s'appliquent :

  • Les deux coureurs commencent à courir dès que l'utilisateur clique sur le bouton Start (Démarrer), c'est-à-dire lorsque les coroutines se lancent.
  • Les deux coureurs marquent une pause ou s'arrêtent lorsque l'utilisateur clique sur le bouton Pause (Mettre en pause) ou Reset (Réinitialiser), c'est-à-dire lorsque les coroutines sont annulées.
  • Lorsque l'utilisateur ferme l'application, l'annulation est gérée correctement, c'est-à-dire que toutes les coroutines sont annulées et liées à un cycle de vie.

Dans le premier atelier de programmation, vous avez découvert que vous ne pouvez appeler une fonction de suspension qu'à partir d'une autre fonction de suspension. Pour appeler des fonctions de suspension de façon sécurisée depuis un composable, vous devez utiliser le composable LaunchedEffect(). LaunchedEffect() exécute la fonction de suspension fournie tant qu'elle reste dans la composition. Vous pouvez utiliser la fonction modulable LaunchedEffect() pour effectuer toutes les opérations ci-dessous :

  • Le composable LaunchedEffect() vous permet d'appeler de façon sécurisée des fonctions de suspension à partir de composables.
  • Lorsque la fonction LaunchedEffect() entre dans la composition, elle lance une coroutine avec le bloc de code transmis en tant que paramètre. Elle exécute la fonction de suspension fournie tant qu'elle reste dans la composition. Lorsqu'un utilisateur clique sur le bouton Start (Démarrer) de l'application Race Tracker, la fonction LaunchedEffect() entre dans la composition et lance une coroutine pour mettre à jour la progression.
  • La coroutine est annulée lorsque LaunchedEffect() quitte la composition. Dans l'application, si l'utilisateur clique sur le bouton Reset (Réinitialiser) ou Pause (Mettre en pause), la fonction LaunchedEffect() est supprimée de la composition et les coroutines sous-jacentes sont annulées.

Avec l'application Race Tracker, vous n'avez pas besoin de spécifier un coordinateur, car LaunchedEffect() s'en charge.

Pour commencer la course, appelez la fonction run() pour chaque coureur et procédez comme suit :

  1. Ouvrez le fichier RaceTrackerApp.kt situé dans le package com.example.racetracker.ui.
  2. Accédez au composable RaceTrackerApp() et ajoutez un appel au composable LaunchedEffect() sur la ligne après la définition de raceInProgress.
@Composable
fun RaceTrackerApp() {
    ...
    var raceInProgress by remember { mutableStateOf(false) }

    LaunchedEffect {
    
    }
    RaceTrackerScreen(...)
}
  1. Si les instances de playerOne ou playerTwo sont remplacées par des instances différentes, LaunchedEffect() doit annuler et relancer les coroutines sous-jacentes. Pour vous en assurer, ajoutez les objets playerOne et playerTwo en tant que key de LaunchedEffect. De la même manière qu'un composable Text() est recomposé lorsque sa valeur de texte change, si l'un des arguments clés de LaunchedEffect() change, la coroutine sous-jacente est annulée et relancée.
LaunchedEffect(playerOne, playerTwo) {
}
  1. Ajoutez un appel aux fonctions playerOne.run() et playerTwo.run().
@Composable
fun RaceTrackerApp() {
    ...
    var raceInProgress by remember { mutableStateOf(false) }

    LaunchedEffect(playerOne, playerTwo) {
        playerOne.run()
        playerTwo.run()
    }
    RaceTrackerScreen(...)
}
  1. Encapsulez le bloc LaunchedEffect() avec une condition if. La valeur initiale de cet état est définie sur "false". La valeur de l'état raceInProgress est définie sur "true" lorsque l'utilisateur clique sur le bouton Start (Démarrer) et que la commande LaunchedEffect() s'exécute.
if (raceInProgress) {
    LaunchedEffect(playerOne, playerTwo) {
        playerOne.run()
        playerTwo.run() 
    }
}
  1. Définissez l'indicateur raceInProgress sur "false" pour terminer la course. Cette valeur est également définie sur "false" lorsque l'utilisateur clique sur Pause (Mettre en pause). Lorsque cette valeur est définie sur "false", LaunchedEffect() assure que toutes les coroutines lancées sont annulées.
LaunchedEffect(playerOne, playerTwo) {
    playerOne.run()
    playerTwo.run()
    raceInProgress = false 
}
  1. Exécutez l'application, puis cliquez sur Start (Démarrer). Le joueur 1 devrait terminer la course avant que le joueur 2 ne commence à courir, comme le montre la vidéo suivante :

fa0630395ee18f21.gif

Cela n'a pas l'air très équitable ! Dans la section suivante, vous lancerez des tâches simultanées afin que les deux joueurs puissent courir en même temps, vous vous familiariserez avec les concepts associés et implémenterez ce comportement.

5. Simultanéité structurée

La façon d'écrire du code à l'aide de coroutines est appelée simultanéité structurée. Ce style de programmation améliore la lisibilité de votre code et le temps de développement. L'idée de la simultanéité structurée est que les coroutines ont une hiérarchie : les tâches peuvent lancer des tâches secondaires, qui peuvent à leur tour lancer des sous-tâches. L'unité de cette hiérarchie est appelée champ d'application de coroutine. Les champs d'application de coroutine doivent toujours être associés à un cycle de vie.

Les API de coroutines sont conçues pour respecter cette simultanéité structurée. Vous ne pouvez appeler une fonction de suspension qu'à partir d'une fonction qui est marquée comme fonction de suspension. Cette limitation assure que vous appelez les fonctions de suspension à partir de constructeurs de coroutines, tels que launch. Ces constructeurs sont eux-mêmes associés à un CoroutineScope.

6. Lancer des tâches simultanées

  1. Pour permettre aux deux joueurs de courir simultanément, vous devez lancer deux coroutines distinctes et déplacer chaque appel à la fonction run() dans ces coroutines. Encapsulez l'appel à la fonction playerOne.run() avec le constructeur launch.
LaunchedEffect(playerOne, playerTwo) {
    launch { playerOne.run() }
    playerTwo.run()
    raceInProgress = false 
}
  1. De même, encapsulez l'appel à la fonction playerTwo.run() avec le constructeur launch. Avec cette modification, l'application lance deux coroutines qui s'exécutent simultanément. Les deux joueurs peuvent désormais courir en même temps.
LaunchedEffect(playerOne, playerTwo) {
    launch { playerOne.run() }
    launch { playerTwo.run() }
    raceInProgress = false 
}
  1. Exécutez l'application, puis cliquez sur Start (Démarrer). Alors que vous vous attendez à ce que la course commence, le texte du bouton revient immédiatement à Start (Démarrer) de manière inattendue.

c46c2aa7c580b27b.png

Lorsque les deux joueurs terminent la course, l'application Race Tracker doit réinitialiser le texte du bouton Pause (Mettre en pause) et le définir sur Start (Démarrer). Toutefois, l'application met à jour raceInProgress immédiatement après le lancement des coroutines sans attendre la fin de la course :

LaunchedEffect(playerOne, playerTwo) {
    launch {playerOne.run() }
    launch {playerTwo.run() }
    raceInProgress = false // This will update the state immediately, without waiting for players to finish run() execution.
}

L'indicateur raceInProgress est mis à jour immédiatement pour les raisons suivantes :

  • La fonction de constructeur launch lance une coroutine pour exécuter playerOne.run() et est immédiatement renvoyée pour exécuter la ligne suivante dans le bloc de code.
  • Le même flux d'exécution se produit avec la deuxième fonction de constructeur launch qui exécute la fonction playerTwo.run().
  • Dès que le deuxième constructeur launch est renvoyé, l'indicateur raceInProgress est mis à jour. Le texte du bouton est alors immédiatement remplacé par Start (Démarrer) et la course ne commence pas.

Champ d'application de coroutine

La fonction de suspension coroutineScope crée un CoroutineScope et appelle le bloc de suspension spécifié avec le champ d'application actuel. Le champ d'application hérite son coroutineContext du champ d'application de LaunchedEffect().

Le champ d'application est renvoyé dès que le bloc donné et toutes ses coroutines enfants sont terminés. Pour l'application RaceTracker, il est renvoyé une fois que les deux objets des joueurs ont terminé d'exécuter la fonction run().

  1. Pour faire en sorte que la fonction run() de playerOne et playerTwo termine l'exécution avant de mettre à jour l'indicateur raceInProgress, encapsulez les deux constructeurs launch avec un bloc coroutineScope.
LaunchedEffect(playerOne, playerTwo) {
    coroutineScope {
        launch { playerOne.run() }
        launch { playerTwo.run() }
    }
    raceInProgress = false
}
  1. Exécutez l'application dans un émulateur ou sur un appareil Android. Vous devriez voir l'écran suivant :

598ee57f8ba58a52.png

  1. Cliquez sur le bouton Start (Démarrer). Le joueur 2 est plus rapide que le joueur 1. Une fois la course terminée, c'est-à-dire lorsque les deux joueurs atteignent 100 % de progression, le texte du bouton Pause (Mettre en pause) devient Start (Démarrer). Vous pouvez cliquer sur le bouton Reset (Réinitialiser) pour réinitialiser la course et réexécuter la simulation. La course est présentée dans la vidéo suivante :

c1035eecc5513c58.gif

Le flux d'exécution est illustré dans le schéma suivant :

cf724160fd66ff21.png

  • Lorsque le bloc LaunchedEffect() s'exécute, le contrôle est transféré au bloc coroutineScope{..}.
  • Le bloc coroutineScope lance les deux coroutines simultanément et attend la fin de leur exécution.
  • Une fois l'exécution terminée, l'indicateur raceInProgress est mis à jour.

Le bloc coroutineScope ne renvoie de résultat et ne se poursuit qu'une fois que le code à l'intérieur du bloc a terminé son exécution. Pour le code situé en dehors du bloc, la présence ou l'absence de simultanéité ne devient qu'un simple détail d'implémentation. Ce style de programmation, appelé simultanéité structurée, offre une approche structurée de la programmation simultanée.

Lorsque vous cliquez sur le bouton Reset (Réinitialiser), une fois la course terminée, les coroutines sont annulées et la progression des deux joueurs est réinitialisée sur 0.

Pour voir comment les coroutines sont annulées lorsque l'utilisateur clique sur le bouton Reset (Réinitialiser), procédez comme suit :

  1. Encapsulez le corps de la méthode run() dans un bloc try-catch, comme indiqué dans l'extrait de code suivant :
suspend fun run() {
    try {
        while (currentProgress < maxProgress) {
            delay(progressDelayMillis)
            currentProgress += progressIncrement
        }
    } catch (e: CancellationException) {
        Log.e("RaceParticipant", "$name: ${e.message}")
        throw e // Always re-throw CancellationException.
    }
}
  1. Exécutez l'application et cliquez sur le bouton Start (Démarrer).
  2. Après quelques incréments de progression, cliquez sur le bouton Réinitialiser.
  3. Vérifiez que le message suivant s'affiche dans Logcat :
Player 1: StandaloneCoroutine was cancelled
Player 2: StandaloneCoroutine was cancelled

7. Écrire des tests unitaires pour tester les coroutines

Le code de tests unitaires utilisant des coroutines nécessite une attention particulière, car leur exécution peut être asynchrone et se produire sur plusieurs threads.

Pour appeler des fonctions de suspension dans les tests, vous devez vous trouver dans une coroutine. Étant donné que les fonctions de test JUnit ne sont pas elles-mêmes des fonctions de suspension, vous devez utiliser le constructeur de coroutine runTest. Ce constructeur fait partie de la bibliothèque kotlinx-coroutines-test et est conçu pour exécuter des tests. Il exécute le texte test dans une nouvelle coroutine.

Comme runTest fait partie de la bibliothèque kotlinx-coroutines-test, vous devez ajouter sa dépendance.

Pour ce faire, procédez comme suit :

  1. Ouvrez le fichier build.gradle.kts du module, situé dans le répertoire app du volet Project (Projet).

e7c9e573c41199c6.png

  1. Faites défiler le fichier vers le bas jusqu'au bloc dependencies{}.
  2. Ajoutez une dépendance à l'aide de la configuration testImplementation dans la bibliothèque kotlinx-coroutines-test.
plugins {
    ...
}

android {
    ...
}

dependencies {
    ...
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4")
}
  1. Dans la barre de notification en haut du fichier build.gradle.kts, cliquez sur Sync Now (Synchroniser maintenant) pour terminer l'importation et la compilation, comme illustré dans la capture d'écran suivante :

1c20fc10750ca60c.png

Une fois la compilation terminée, vous pouvez commencer à écrire des tests.

Implémenter des tests unitaires pour commencer et terminer la course

Pour que la progression de la course se mette correctement à jour pendant les différentes phases de la course, vos tests unitaires doivent couvrir différents scénarios. Dans cet atelier de programmation, nous allons étudier deux scénarios :

  • Progression après le début de la course.
  • Progression une fois la course terminée.

Pour vérifier si la progression de la course se met à jour correctement après le début de la course, vous devez confirmer que la progression actuelle est définie sur 1 après la fin de la durée de raceParticipant.progressDelayMillis.

Pour implémenter le scénario de test, procédez comme suit :

  1. Accédez au fichier RaceParticipantTest.kt situé sous l'ensemble de sources de test.
  2. Pour définir le test, après la définition raceParticipant, créez une fonction raceParticipant_RaceStarted_ProgressUpdated() et marquez-la avec l'annotation @Test. Comme le bloc de test doit être placé dans le constructeur runTest, utilisez la syntaxe d'expression pour renvoyer le bloc runTest() en tant que résultat de test.
class RaceParticipantTest {
    private val raceParticipant = RaceParticipant(
        ...
    )

    @Test
    fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    }
}
  1. Ajoutez une variable expectedProgress en lecture seule et définissez-la sur 1.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
}
  1. Pour simuler le début d'une course, utilisez le constructeur launch pour lancer une nouvelle coroutine et appeler la fonction raceParticipant.run().
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
}

La valeur de la propriété raceParticipant.progressDelayMillis détermine le délai à l'issue duquel la progression de la course est mise à jour. Pour pouvoir tester la progression une fois le délai de progressDelayMillis écoulé, ajoutez un certain délai à votre test.

  1. Utilisez la fonction d'assistance advanceTimeBy() pour avancer le temps de la valeur de raceParticipant.progressDelayMillis. La fonction advanceTimeBy() permet de réduire la durée d'exécution du test.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
}
  1. Comme advanceTimeBy() n'exécute pas la tâche planifiée à la durée donnée, vous devez appeler la fonction runCurrent(). Cette fonction exécute toutes les tâches en attente à l'heure actuelle.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
    runCurrent()
}
  1. Pour que la progression soit mise à jour, ajoutez un appel à la fonction assertEquals() afin de vérifier si la valeur de la propriété raceParticipant.currentProgress correspond à celle de la variable expectedProgress.
@Test
fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
    val expectedProgress = 1
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.progressDelayMillis)
    runCurrent()
    assertEquals(expectedProgress, raceParticipant.currentProgress)
}
  1. Exécutez le test pour vérifier qu'il a réussi.

Pour vérifier si la progression se met à jour correctement une fois la course terminée, vous devez confirmer que la progression actuelle est définie sur 100.

Pour implémenter le test, procédez comme suit :

  1. Après la fonction de test raceParticipant_RaceStarted_ProgressUpdated(), créez une fonction raceParticipant_RaceFinished_ProgressUpdated() et marquez-la avec l'annotation @Test. La fonction doit renvoyer un résultat de test à partir du bloc runTest{}.
class RaceParticipantTest {
    ...

    @Test
    fun raceParticipant_RaceStarted_ProgressUpdated() = runTest {
        ...
    }

    @Test
    fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    }
}
  1. Utilisez le constructeur launch pour lancer une nouvelle coroutine et ajouter un appel à la fonction raceParticipant.run() qu'elle contient.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
}
  1. Pour simuler la fin de la course, utilisez la fonction advanceTimeBy() afin de faire avancer le temps du coordinateur de raceParticipant.maxProgress * raceParticipant.progressDelayMillis :
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
}
  1. Ajoutez un appel à la fonction runCurrent() pour exécuter les tâches en attente.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
    runCurrent()
}
  1. Pour vous assurer que la progression se met à jour correctement, ajoutez un appel à la fonction assertEquals() afin de vérifier que la valeur de la propriété raceParticipant.currentProgress est égale à 100.
@Test
fun raceParticipant_RaceFinished_ProgressUpdated() = runTest {
    launch { raceParticipant.run() }
    advanceTimeBy(raceParticipant.maxProgress * raceParticipant.progressDelayMillis)
    runCurrent()
    assertEquals(100, raceParticipant.currentProgress)
}
  1. Exécutez le test pour vérifier qu'il a réussi.

Relevez ce défi !

Appliquez les stratégies de test décrites dans l'atelier de programmation Écrire des tests unitaires pour ViewModel. Ajoutez les tests pour découvrir les scénarios de réussite, d'erreur et de cas limite.

Comparez le test que vous écrivez à ceux disponibles dans le code de solution.

8. Télécharger le code de solution

Pour télécharger le code de cet atelier de programmation terminé, utilisez les commandes Git suivantes :

git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-race-tracker.git
cd basic-android-kotlin-compose-training-race-tracker

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous souhaitez voir le code de solution, affichez-le sur GitHub.

9. Conclusion

Félicitations ! Vous savez maintenant utiliser des coroutines pour gérer la simultanéité. 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. Vous avez également appris à écrire des tests unitaires pour tester les coroutines.

Voici quelques-uns des avantages des coroutines :

  • Lisibilité : le code que vous écrivez avec des coroutines permet de comprendre clairement la séquence d'exécution des lignes de code.
  • Intégration Jetpack : de nombreuses bibliothèques Jetpack, telles que Compose et ViewModel, 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.
  • Simultanéité structurée : les coroutines rendent le code simultané sûr et facile à implémenter, éliminent le code récurrent inutile et assurent que les coroutines lancées par l'application ne sont pas perdues et qu'elles ne continuent pas à épuiser des ressources.

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.
  • Le mot clé suspend sert à marquer une fonction ou un type de fonction pour indiquer sa disponibilité à exécuter, suspendre et reprendre l'exécution d'un ensemble d'instructions dans le code.
  • Une fonction suspend ne peut être appelée qu'à partir d'une autre fonction de suspension.
  • Vous pouvez démarrer une nouvelle coroutine à l'aide de la fonction de constructeur launch ou async.
  • Le contexte de coroutine, les constructeurs de coroutines, la tâche, le champ d'application de coroutine et le coordinateur sont les principaux composants de l'implémentation des coroutines.
  • Les coroutines utilisent les coordinateurs pour déterminer les threads à utiliser pour son exécution.
  • La tâche joue un rôle important pour assurer la simultanéité structurée en gérant le cycle de vie des coroutines et en maintenant la relation parent-enfant.
  • Un CoroutineContext définit le comportement d'une coroutine à l'aide d'une tâche et d'un coordinateur de coroutine.
  • 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.
  • Le lancement, l'exécution, l'annulation et l'échec sont quatre opérations courantes dans l'exécution d'une coroutine.
  • Les coroutines suivent un principe de simultanéité structurée.

En savoir plus