Utiliser des coroutines Kotlin dans votre application Android

Dans cet atelier de programmation, vous apprendrez à utiliser les coroutines Kotlin dans une application Android. Il s'agit d'une nouvelle façon de gérer les threads d'arrière-plan qui peut simplifier le code en réduisant le besoin de rappels. Les coroutines sont une fonctionnalité Kotlin qui convertit les rappels asynchrones pour les tâches de longue durée, telles que l'accès à une base de données ou à un réseau, en code séquentiel.

Voici un extrait de code pour vous donner une idée de ce que vous allez faire.

// Async callbacks
networkRequest { result ->
   // Successful network request
   databaseSave(result) { rows ->
     // Result saved
   }
}

Le code basé sur des rappels sera converti en code séquentiel à l'aide de coroutines.

// The same code with coroutines
val result = networkRequest()
// Successful network request
databaseSave(result)
// Result saved

Vous allez commencer à partir d'une application existante, créée à l'aide des composants d'architecture, qui utilise un style de rappel pour les tâches de longue durée.

À la fin de cet atelier de programmation, vous aurez suffisamment d'expérience pour utiliser des coroutines dans votre application afin de charger des données depuis le réseau et vous pourrez intégrer des coroutines dans une application. Vous connaîtrez également les bonnes pratiques pour les coroutines et saurez comment écrire un test pour le code qui utilise des coroutines.

Prérequis

  • Bonne connaissance des composants d'architecture ViewModel, LiveData, Repository et Room
  • Expérience de la syntaxe Kotlin, y compris des fonctions d'extension et des lambdas
  • Connaissances de base de l'utilisation des threads sur Android, y compris le thread principal, les threads en arrière-plan et les rappels

Objectifs de l'atelier

  • Appelez le code écrit avec des coroutines et obtenez les résultats.
  • Utilisez des fonctions suspend pour rendre le code asynchrone séquentiel.
  • Utilisez launch et runBlocking pour contrôler l'exécution du code.
  • Découvrez des techniques pour convertir des API existantes en coroutines à l'aide de suspendCoroutine.
  • Utilisez des coroutines avec les composants d'architecture.
  • Découvrez les bonnes pratiques pour tester les coroutines.

Prérequis

  • Android Studio 3.5 (il se peut que l'atelier de programmation fonctionne avec d'autres versions, mais que des éléments soient manquants ou différents).

Si vous rencontrez des problèmes (bugs de code, erreurs grammaticales, formulation peu claire, etc.) au cours de cet atelier de programmation, veuillez les signaler via le lien Signaler une erreur situé dans l'angle inférieur gauche de l'atelier de programmation.

Télécharger le code

Cliquez sur le lien ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation :

Télécharger le fichier ZIP

Vous pouvez également cloner le dépôt GitHub à partir de la ligne de commande à l'aide de la commande suivante :

$ git clone https://github.com/googlecodelabs/kotlin-coroutines.git

Questions fréquentes

Voyons d'abord comment se présente notre exemple d'application à l'état d'origine. Suivez les instructions ci-dessous pour ouvrir l'exemple d'application dans Android Studio :

  1. Si vous avez téléchargé le fichier ZIP kotlin-coroutines, décompressez-le.
  2. Ouvrez le projet coroutines-codelab dans Android Studio.
  3. Sélectionnez le module d'application start.
  4. Cliquez sur le bouton execute.pngExécuter, puis sélectionnez un émulateur ou connectez votre appareil Android, qui doit être en mesure d'exécuter Android Lollipop (SDK 21 au minimum). L'écran "Kotlin Coroutines" (Coroutines Kotlin) devrait s'afficher :

Cette application de démarrage utilise des threads pour incrémenter le nombre après un court délai lorsque vous appuyez sur l'écran. Il récupère également un nouveau titre sur le réseau et l'affiche à l'écran. Essayez maintenant. Le nombre et le message devraient changer après un court délai. Dans cet atelier de programmation, vous allez convertir cette application pour qu'elle utilise des coroutines.

Cette application utilise des composants d'architecture pour séparer le code de l'UI dans MainActivity de la logique d'application dans MainViewModel. Prenez quelques instants pour vous familiariser avec la structure du projet.

  1. MainActivity affiche l'UI, enregistre les écouteurs de clics et peut afficher un Snackbar. Il transmet les événements à MainViewModel et met à jour l'écran en fonction de LiveData dans MainViewModel.
  2. MainViewModel gère les événements dans onMainViewClicked et communique avec MainActivity à l'aide de LiveData..
  3. Executors définit BACKGROUND,, qui peut exécuter des éléments sur un thread en arrière-plan.
  4. TitleRepository récupère les résultats du réseau et les enregistre dans la base de données.

Ajouter des coroutines à un projet

Pour utiliser des coroutines en Kotlin, vous devez inclure la bibliothèque coroutines-core dans le fichier build.gradle (Module: app) de votre projet. Les projets de l'atelier de programmation l'ont déjà fait pour vous. Vous n'avez donc pas besoin de le faire pour terminer l'atelier.

Les coroutines sur Android sont disponibles en tant que bibliothèque principale et extensions spécifiques à Android :

  • kotlinx-coroutines-core  : interface principale pour utiliser les coroutines en Kotlin
  • kotlinx-coroutines-android  : compatibilité avec le thread principal Android dans les coroutines

L'application de démarrage inclut déjà les dépendances dans build.gradle.. Lorsque vous créez un projet d'application, vous devez ouvrir build.gradle (Module: app) et ajouter les dépendances des coroutines au projet.

dependencies {
  ...
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:x.x.x"
  implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:x.x.x"
}

Sur Android, il est essentiel d'éviter de bloquer le thread principal. Le thread principal est un thread unique qui gère toutes les mises à jour de l'UI. C'est également le thread qui appelle tous les gestionnaires de clics et autres rappels d'UI. Il doit donc fonctionner de manière fluide pour garantir une expérience utilisateur de qualité.

Pour que votre application s'affiche sur l'appareil de l'utilisateur sans aucune pause visible, le thread principal doit mettre à jour l'écran toutes les 16 ms au plus, soit environ 60 images par seconde. La plupart des tâches courantes (comme l'analyse de grands ensembles de données JSON, l'écriture de données dans une base de données ou la récupération de données sur le réseau) prennent plus de temps. Par conséquent, appeler du code comme celui-ci à partir du thread principal peut entraîner la mise en pause, le stuttering voire le blocage de l'application. Si vous bloquez le thread principal trop longtemps, l'application risque même de planter et d'afficher une boîte de dialogue L'application ne répond pas.

Regardez la vidéo ci-dessous pour découvrir comment les coroutines résolvent ce problème sur Android en introduisant la sécurité du thread principal.

Modèle de rappel

Les rappels sont un modèle permettant de réaliser des tâches de longue durée sans bloquer le thread principal. Grâce aux rappels, vous pouvez démarrer ces tâches sur un thread en arrière-plan. Une fois la tâche terminée, le rappel est appelé pour vous informer du résultat sur le thread principal.

Consultez un exemple du modèle de rappel.

// Slow request with callbacks
@UiThread
fun makeNetworkRequest() {
    // The slow network request runs on another thread
    slowFetch { result ->
        // When the result is ready, this callback will get the result
        show(result)
    }
    // makeNetworkRequest() exits after calling slowFetch without waiting for the result
}

Comme ce code est annoté avec @UiThread, il doit s'exécuter assez rapidement pour s'exécuter sur le thread principal. Cela signifie qu'il doit renvoyer les données très rapidement pour que la mise à jour de l'écran suivant ne soit pas retardée. Cependant, comme slowFetch prendra des secondes, voire des minutes, à s'exécuter, le thread principal ne peut pas attendre le résultat. Le rappel show(result) permet à slowFetch de s'exécuter sur un thread d'arrière-plan et de renvoyer le résultat lorsqu'il est prêt.

Utiliser des coroutines pour supprimer les rappels

Bien qu'ils soient pratiques, les rappels présentent cependant quelques inconvénients. Un code utilisant beaucoup de rappels peut devenir difficile à lire et peut compliquer le raisonnement. De plus, les rappels ne permettent pas d'utiliser certaines fonctionnalités de langage, comme les exceptions.

Les coroutines Kotlin permettent de convertir en code séquentiel un code basé sur des rappels. Le code écrit de manière séquentielle est généralement plus facile à lire et peut même utiliser des fonctionnalités de langage telles que les exceptions.

Au final, ils remplissent la même fonction : attendre que le résultat d'une tâche de longue durée soit disponible et poursuivre l'exécution. Toutefois, dans le code, ils sont très différents.

Le mot clé suspend permet à Kotlin de marquer une fonction ou un type de fonction disponible pour les coroutines. Lorsqu'une coroutine appelle une fonction marquée suspend, au lieu de bloquer jusqu'à ce que cette fonction renvoie un résultat comme un appel de fonction normal, elle suspend l'exécution jusqu'à ce que le résultat soit prêt, puis elle reprend là où elle s'était arrêtée avec le résultat. Lorsqu'elle est suspendue en attente d'un résultat, elle débloque le thread sur lequel elle s'exécute afin que d'autres fonctions ou coroutines puissent s'exécuter.

Par exemple, dans le code ci-dessous, makeNetworkRequest() et slowFetch() sont toutes les deux des fonctions suspend.

// Slow request with coroutines
@UiThread
suspend fun makeNetworkRequest() {
    // slowFetch is another suspend function so instead of 
    // blocking the main thread  makeNetworkRequest will `suspend` until the result is 
    // ready
    val result = slowFetch()
    // continue to execute after the result is ready
    show(result)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }

Comme pour la version de rappel, makeNetworkRequest doit renvoyer immédiatement à partir du thread principal, car il est marqué @UiThread. Cela signifie qu'il ne pouvait généralement pas appeler de méthodes de blocage telles que slowFetch. C'est là que le mot clé suspend fait son effet.

Par rapport au code basé sur des rappels, le code de coroutine permet d'obtenir le même résultat de déblocage du thread actuel avec moins de code. Grâce à son style séquentiel, il est facile d'enchaîner plusieurs tâches de longue durée sans créer plusieurs rappels. Par exemple, le code qui récupère un résultat à partir de deux points de terminaison réseau et l'enregistre dans la base de données peut être écrit sous forme de fonction dans des coroutines sans rappels. Par exemple :

// Request data from network and save it to database with coroutines

// Because of the @WorkerThread, this function cannot be called on the
// main thread without causing an error.
@WorkerThread
suspend fun makeNetworkRequest() {
    // slowFetch and anotherFetch are suspend functions
    val slow = slowFetch()
    val another = anotherFetch()
    // save is a regular function and will block this thread
    database.save(slow, another)
}

// slowFetch is main-safe using coroutines
suspend fun slowFetch(): SlowResult { ... }
// anotherFetch is main-safe using coroutines
suspend fun anotherFetch(): AnotherResult { ... }

Dans la section suivante, vous allez introduire des coroutines dans l'application exemple.

Dans cet exercice, vous allez écrire une coroutine pour afficher un message après un délai. Pour commencer, assurez-vous que le module start est ouvert dans Android Studio.

Comprendre CoroutineScope

En Kotlin, toutes les coroutines s'exécutent dans un CoroutineScope. Une "scope" ou "portée" permet de contrôler la durée de vie des coroutines tout au long de sa tâche. Lorsque vous annulez la tâche d'une portée, cette action annule toutes les coroutines démarrées dans celle-ci. Sur Android, vous pouvez utiliser un champ d'application pour annuler toutes les coroutines en cours d'exécution lorsque, par exemple, l'utilisateur quitte un Activity ou un Fragment. Les portées vous permettent également de spécifier un répartiteur par défaut. Un répartiteur contrôle le thread sur lequel une coroutine s'exécute.

Pour les coroutines démarrées par l'UI, il est généralement correct de les démarrer sur Dispatchers.Main, qui est le thread principal sur Android. Une coroutine démarrée sur Dispatchers.Main ne bloquera pas le thread principal lorsqu'elle sera suspendue. Étant donné qu'une coroutine ViewModel met presque toujours à jour l'UI sur le thread principal, le fait de démarrer des coroutines sur le thread principal vous évite des changements de thread supplémentaires. Une coroutine démarrée sur le thread principal peut changer de répartiteur à tout moment après son démarrage. Par exemple, il peut utiliser un autre répartiteur pour analyser un résultat JSON volumineux en dehors du thread principal.

Utiliser viewModelScope

La bibliothèque AndroidX lifecycle-viewmodel-ktx ajoute un CoroutineScope aux ViewModels qui est configuré pour démarrer les coroutines liées à l'UI. Pour utiliser cette bibliothèque, vous devez l'inclure dans le fichier build.gradle (Module: start) de votre projet. Cette étape a déjà été effectuée dans les projets de l'atelier de programmation.

dependencies {
  ...
  implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:x.x.x"
}

La bibliothèque ajoute un viewModelScope en tant que fonction d'extension de la classe ViewModel. Ce champ d'application est lié à Dispatchers.Main et sera automatiquement annulé lorsque le ViewModel sera effacé.

Passer des threads aux coroutines

Dans MainViewModel.kt, recherchez le prochain TODO avec ce code :

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   BACKGROUND.submit {
       Thread.sleep(1_000)
       _taps.postValue("$tapCount taps")
   }
}

Ce code utilise BACKGROUND ExecutorService (défini dans util/Executor.kt) pour s'exécuter dans un thread d'arrière-plan. Étant donné que sleep bloque le thread actuel, l'UI se figerait s'il était appelé sur le thread principal. Une seconde après que l'utilisateur a cliqué sur la vue principale, une snackbar est demandée.

Pour le constater, supprimez le BACKGROUND du code et exécutez-le à nouveau. L'icône de chargement ne s'affichera pas et tout "sautera" à l'état final une seconde plus tard.

MainViewModel.kt

/**
* Wait one second then update the tap count.
*/
private fun updateTaps() {
   // TODO: Convert updateTaps to use coroutines
   tapCount++
   Thread.sleep(1_000)
   _taps.postValue("$tapCount taps")
}

Remplacez updateTaps par le code basé sur la coroutine qui fait la même chose. Vous devrez importer launch et delay.

MainViewModel.kt

/**
* Wait one second then display a snackbar.
*/
fun updateTaps() {
   // launch a coroutine in viewModelScope
   viewModelScope.launch {
       tapCount++
       // suspend this coroutine for one second
       delay(1_000)
       // resume in the main dispatcher
       // _snackbar.value can be called directly from main thread
       _taps.postValue("$tapCount taps")
   }
}

Ce code fait la même chose, en attendant une seconde avant d'afficher une snackbar. Cependant, il existe quelques différences importantes :

  1. viewModelScope.launch démarrera une coroutine dans viewModelScope. Cela signifie que lorsque le job que nous avons transmis à viewModelScope est annulé, toutes les coroutines de ce job/champ d'application sont annulées. Si l'utilisateur a quitté l'activité avant le retour de delay, cette coroutine sera automatiquement annulée lorsque onCleared sera appelé lors de la destruction du ViewModel.
  2. Comme viewModelScope a un répartiteur par défaut de Dispatchers.Main, cette coroutine sera lancée dans le thread principal. Nous verrons plus tard comment utiliser différents threads.
  3. La fonction delay est une fonction suspend. Dans Android Studio, cela est indiqué par l'icône  dans la marge de gauche. Même si cette coroutine s'exécute sur le thread principal, delay ne bloquera pas le thread pendant une seconde. Au lieu de cela, le répartiteur planifiera la reprise de la coroutine dans une seconde à la prochaine instruction.

Exécutez-le. Lorsque vous cliquez sur la vue principale, une snackbar devrait s'afficher une seconde plus tard.

Dans la section suivante, nous verrons comment tester cette fonction.

Dans cet exercice, vous allez écrire un test pour le code que vous venez d'écrire. Cet exercice vous montre comment tester les coroutines s'exécutant sur Dispatchers.Main à l'aide de la bibliothèque kotlinx-coroutines-test. Plus loin dans cet atelier de programmation, vous implémenterez un test qui interagit directement avec les coroutines.

Examiner le code existant

Ouvrez MainViewModelTest.kt dans le dossier androidTest.

MainViewModelTest.kt

class MainViewModelTest {
   @get:Rule
   val coroutineScope =  MainCoroutineScopeRule()
   @get:Rule
   val instantTaskExecutorRule = InstantTaskExecutorRule()

   lateinit var subject: MainViewModel

   @Before
   fun setup() {
       subject = MainViewModel(
           TitleRepository(
                   MainNetworkFake("OK"),
                   TitleDaoFake("initial")
           ))
   }
}

Une règle permet d'exécuter du code avant et après l'exécution d'un test dans JUnit. Deux règles nous permettent de tester MainViewModel dans un test hors appareil :

  1. InstantTaskExecutorRule est une règle JUnit qui configure LiveData pour exécuter chaque tâche de manière synchrone.
  2. MainCoroutineScopeRule est une règle personnalisée dans cette codebase qui configure Dispatchers.Main pour utiliser un TestCoroutineDispatcher à partir de kotlinx-coroutines-test. Cela permet aux tests de faire avancer une horloge virtuelle pour les tests et au code d'utiliser Dispatchers.Main dans les tests unitaires.

Dans la méthode setup, une nouvelle instance de MainViewModel est créée à l'aide de faux tests. Il s'agit d'implémentations fictives du réseau et de la base de données fournies dans le code de démarrage pour vous aider à écrire des tests sans utiliser le véritable réseau ni la véritable base de données.

Pour ce test, les faux ne sont nécessaires que pour satisfaire les dépendances de MainViewModel. Dans la suite de cet atelier de programmation, vous mettrez à jour les faux pour qu'ils soient compatibles avec les coroutines.

Écrire un test qui contrôle les coroutines

Ajoutez un test qui garantit que les appuis sont mis à jour une seconde après avoir cliqué sur la vue principale :

MainViewModelTest.kt

@Test
fun whenMainClicked_updatesTaps() {
   subject.onMainViewClicked()
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("0 taps")
   coroutineScope.advanceTimeBy(1000)
   Truth.assertThat(subject.taps.getValueForTest()).isEqualTo("1 taps")
}

En appelant onMainViewClicked, la coroutine que nous venons de créer sera lancée. Ce test vérifie que le texte"Taps" (Appuis) reste à 0 taps juste après l'appel de onMainViewClicked, puis qu'il passe à 1 taps une seconde plus tard.

Ce test utilise virtual-time pour contrôler l'exécution de la coroutine lancée par onMainViewClicked. MainCoroutineScopeRule vous permet de mettre en pause, de reprendre ou de contrôler l'exécution des coroutines lancées sur Dispatchers.Main. Ici, nous appelons advanceTimeBy(1_000), ce qui entraînera l'exécution immédiate par le répartiteur principal des coroutines dont la reprise est prévue une seconde plus tard.

Ce test est entièrement déterministe, ce qui signifie qu'il s'exécutera toujours de la même manière. De plus, comme il contrôle entièrement l'exécution des coroutines lancées sur Dispatchers.Main, il n'a pas besoin d'attendre une seconde pour que la valeur soit définie.

Exécuter le test existant

  1. Effectuez un clic droit sur le nom de la classe MainViewModelTest dans votre éditeur pour ouvrir un menu contextuel.
  2. Dans le menu contextuel, sélectionnez execute.pngExécuter "MainViewModelTest".
  3. Pour les exécutions ultérieures, vous pouvez sélectionner cette configuration de test dans les configurations à côté du bouton execute.png de la barre d'outils. Par défaut, la configuration s'appellera MainViewModelTest.

Le test devrait réussir. Son exécution devrait prendre beaucoup moins d'une seconde.

Dans le prochain exercice, vous apprendrez à convertir des API de rappel existantes pour utiliser des coroutines.

Dans cette étape, vous allez commencer à convertir un dépôt pour utiliser des coroutines. Pour ce faire, nous allons ajouter des coroutines à ViewModel, Repository, Room et Retrofit.

Avant de passer aux coroutines, il est judicieux de comprendre le rôle de chaque partie de l'architecture.

  1. MainDatabase implémente une base de données à l'aide de Room qui enregistre et charge un Title.
  2. MainNetwork implémente une API réseau qui récupère un nouveau titre. Il utilise Retrofit pour récupérer les titres. Retrofit est configuré pour renvoyer des erreurs ou des données fictives de manière aléatoire, mais se comporte sinon comme s'il effectuait de véritables requêtes réseau.
  3. TitleRepository implémente une seule API pour récupérer ou actualiser le titre en combinant les données du réseau et de la base de données.
  4. MainViewModel représente l'état de l'écran et gère les événements. Il indiquera au dépôt d'actualiser le titre lorsque l'utilisateur appuiera sur l'écran.

Étant donné que la requête réseau est déclenchée par des événements d'UI et que nous voulons démarrer une coroutine en fonction de ces événements, l'endroit naturel pour commencer à utiliser des coroutines est dans ViewModel.

Version avec rappel

Ouvrez MainViewModel.kt pour afficher la déclaration de refreshTitle.

MainViewModel.kt

/**
* Update title text via this LiveData
*/
val title = repository.title


// ... other code ...


/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   // TODO: Convert refreshTitle to use coroutines
   _spinner.value = true
   repository.refreshTitleWithCallbacks(object: TitleRefreshCallback {
       override fun onCompleted() {
           _spinner.postValue(false)
       }

       override fun onError(cause: Throwable) {
           _snackBar.postValue(cause.message)
           _spinner.postValue(false)
       }
   })
}

Cette fonction est appelée chaque fois que l'utilisateur clique sur l'écran. Elle entraîne l'actualisation du titre par le dépôt et l'écriture du nouveau titre dans la base de données.

Cette implémentation utilise un rappel pour effectuer plusieurs actions :

  • Avant de lancer une requête, il affiche un indicateur de chargement avec _spinner.value = true.
  • Lorsqu'il obtient un résultat, il efface le spinner de chargement avec _spinner.value = false.
  • En cas d'erreur, il demande à un snackbar de s'afficher et efface le spinner.

Notez que le rappel onCompleted n'est pas transmis à title. Comme nous écrivons tous les titres dans la base de données Room, l'UI est mise à jour avec le titre actuel en observant un LiveData qui est mis à jour par Room.

Dans la mise à jour des coroutines, nous conserverons exactement le même comportement. Il est judicieux d'utiliser une source de données observable, comme une base de données Room, pour que l'UI reste automatiquement à jour.

Version des coroutines

Réécrivons refreshTitle avec des coroutines !

Comme nous en aurons besoin immédiatement, créons une fonction de suspension vide dans notre dépôt (TitleRespository.kt). Définissez une nouvelle fonction qui utilise l'opérateur suspend pour indiquer à Kotlin qu'elle fonctionne avec les coroutines.

TitleRepository.kt

suspend fun refreshTitle() {
    // TODO: Refresh from network and write to database
    delay(500)
}

Lorsque vous aurez terminé cet atelier de programmation, vous mettrez à jour ce code pour utiliser Retrofit et Room afin de récupérer un nouveau titre et de l'écrire dans la base de données à l'aide de coroutines. Pour l'instant, il passera simplement 500 millisecondes à faire semblant de travailler, puis continuera.

Dans MainViewModel, remplacez la version de rappel de refreshTitle par une version qui lance une nouvelle coroutine :

MainViewModel.kt

/**
* Refresh the title, showing a loading spinner while it refreshes and errors via snackbar.
*/
fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           repository.refreshTitle()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Examinons cette fonction :

viewModelScope.launch {

Comme pour la coroutine permettant de mettre à jour le nombre de taps, commencez par lancer une nouvelle coroutine dans viewModelScope. Cela utilisera Dispatchers.Main, ce qui est correct. Bien que refreshTitle effectue une requête réseau et une requête de base de données, il peut utiliser des coroutines pour exposer une interface main-safe. Cela signifie qu'il pourra être appelé en toute sécurité depuis le thread principal.

Comme nous utilisons viewModelScope, lorsque l'utilisateur quitte cet écran, le travail lancé par cette coroutine est automatiquement annulé. Cela signifie qu'il n'effectuera pas de requêtes réseau ni de requêtes de base de données supplémentaires.

Les lignes de code suivantes appellent réellement refreshTitle dans repository.

try {
    _spinner.value = true
    repository.refreshTitle()
}

Avant que cette coroutine ne fasse quoi que ce soit, elle démarre le spinner de chargement, puis appelle refreshTitle comme une fonction normale. Cependant, comme refreshTitle est une fonction de suspension, son exécution est différente de celle d'une fonction normale.

Nous n'avons pas besoin de transmettre un rappel. La coroutine sera suspendue jusqu'à ce qu'elle soit reprise par refreshTitle. Bien qu'elle ressemble à un appel de fonction de blocage ordinaire, elle attend automatiquement que la requête réseau et la requête de base de données soient terminées avant de reprendre sans bloquer le thread principal.

} catch (error: TitleRefreshError) {
    _snackBar.value = error.message
} finally {
    _spinner.value = false
}

Les exceptions dans les fonctions de suspension fonctionnent exactement comme les erreurs dans les fonctions régulières. Si vous générez une erreur dans une fonction de suspension, elle sera générée pour l'appelant. Ainsi, même si leur exécution est très différente, vous pouvez utiliser des blocs try/catch standards pour les gérer. Cela est utile, car cela vous permet de vous appuyer sur la prise en charge linguistique intégrée pour la gestion des erreurs au lieu de créer une gestion des erreurs personnalisée pour chaque rappel.

De plus, si vous générez une exception à partir d'une coroutine, celle-ci annulera son parent par défaut. Cela signifie qu'il est facile d'annuler plusieurs tâches associées en même temps.

Ensuite, dans un bloc "finally", nous pouvons nous assurer que le spinner est toujours désactivé après l'exécution de la requête.

Exécutez à nouveau l'application en sélectionnant la configuration start, puis en appuyant surexecute.png. Un indicateur de chargement devrait s'afficher lorsque vous appuyez n'importe où. Le titre restera le même, car nous n'avons pas encore connecté notre réseau ni notre base de données.

Dans l'exercice suivant, vous allez mettre à jour le dépôt pour qu'il effectue réellement des tâches.

Dans cet exercice, vous allez apprendre à changer le thread sur lequel une coroutine s'exécute afin d'implémenter une version fonctionnelle de TitleRepository.

Examiner le code de rappel existant dans refreshTitle

Ouvrez TitleRepository.kt et examinez l'implémentation existante basée sur les rappels.

TitleRepository.kt

// TitleRepository.kt

fun refreshTitleWithCallbacks(titleRefreshCallback: TitleRefreshCallback) {
   // This request will be run on a background thread by retrofit
   BACKGROUND.submit {
       try {
           // Make network request using a blocking call
           val result = network.fetchNextTitle().execute()
           if (result.isSuccessful) {
               // Save it to database
               titleDao.insertTitle(Title(result.body()!!))
               // Inform the caller the refresh is completed
               titleRefreshCallback.onCompleted()
           } else {
               // If it's not successful, inform the callback of the error
               titleRefreshCallback.onError(
                       TitleRefreshError("Unable to refresh title", null))
           }
       } catch (cause: Throwable) {
           // If anything throws an exception, inform the caller
           titleRefreshCallback.onError(
                   TitleRefreshError("Unable to refresh title", cause))
       }
   }
}

Dans TitleRepository.kt, la méthode refreshTitleWithCallbacks est implémentée avec un rappel pour communiquer l'état de chargement et d'erreur à l'appelant.

Cette fonction effectue plusieurs opérations pour implémenter l'actualisation.

  1. Passer à un autre fil de discussion avec BACKGROUND ExecutorService
  2. Exécutez la requête réseau fetchNextTitle à l'aide de la méthode de blocage execute(). Cela exécutera la requête réseau dans le thread actuel, dans ce cas, l'un des threads de BACKGROUND.
  3. Si le résultat est positif, enregistrez-le dans la base de données avec insertTitle et appelez la méthode onCompleted().
  4. Si le résultat n'est pas concluant ou qu'une exception se produit, appelez la méthode onError pour informer l'appelant de l'échec de l'actualisation.

Cette implémentation basée sur un rappel est main-safe, car elle ne bloque pas le thread principal. Toutefois, il doit utiliser un rappel pour informer l'appelant lorsque le travail est terminé. Il appelle également les rappels sur le thread BACKGROUND sur lequel il est passé.

Appeler des fonctions bloquantes depuis des coroutines

Sans introduire de coroutines dans le réseau ni dans la base de données, nous pouvons rendre ce code main-safe à l'aide de coroutines. Cela nous permettra de nous débarrasser du rappel et de renvoyer le résultat au thread qui l'a initialement appelé.

Vous pouvez utiliser ce modèle chaque fois que vous avez besoin d'effectuer un travail bloquant ou intensif en termes de processeur à partir d'une coroutine, comme trier et filtrer une longue liste ou lire à partir du disque.

Pour passer d'un répartiteur à l'autre, les coroutines utilisent withContext. Appeler withContext permet de basculer vers l'autre coordinateur seulement pour le lambda, puis de revenir avec le résultat de ce lambda au coordinateur qui l'a appelé.

Par défaut, les coroutines Kotlin proposent trois Dispatchers : Main, IO et Default. Le coordinateur IO est optimisé pour les tâches d'E/S comme la lecture à partir du réseau ou du disque, tandis que le coordinateur Default est optimisé pour les tâches sollicitant le processeur de manière intensive.

TitleRepository.kt

suspend fun refreshTitle() {
   // interact with *blocking* network and IO calls from a coroutine
   withContext(Dispatchers.IO) {
       val result = try {
           // Make network request using a blocking call
           network.fetchNextTitle().execute()
       } catch (cause: Throwable) {
           // If the network throws an exception, inform the caller
           throw TitleRefreshError("Unable to refresh title", cause)
       }
      
       if (result.isSuccessful) {
           // Save it to database
           titleDao.insertTitle(Title(result.body()!!))
       } else {
           // If it's not successful, inform the callback of the error
           throw TitleRefreshError("Unable to refresh title", null)
       }
   }
}

Cette implémentation utilise des appels bloquants pour le réseau et la base de données, mais elle reste un peu plus simple que la version de rappel.

Ce code utilise toujours des appels bloquants. L'appel de execute() et de insertTitle(...) bloquera le thread dans lequel cette coroutine s'exécute. Cependant, en passant à Dispatchers.IO à l'aide de withContext, nous bloquons l'un des threads du répartiteur d'E/S. La coroutine qui a appelé cette fonction, qui s'exécute peut-être sur Dispatchers.Main, sera suspendue jusqu'à ce que le lambda withContext soit terminé.

Par rapport à la version avec rappel, il existe deux différences importantes :

  1. withContext renvoie son résultat au Dispatcher qui l'a appelé, en l'occurrence Dispatchers.Main. La version de rappel appelait les rappels sur un thread dans le service d'exécution BACKGROUND.
  2. L'appelant n'a pas besoin de transmettre un rappel à cette fonction. Ils peuvent s'appuyer sur la suspension et la reprise pour obtenir le résultat ou l'erreur.

Exécuter à nouveau l'application

Si vous exécutez à nouveau l'application, vous verrez que la nouvelle implémentation basée sur les coroutines charge les résultats du réseau.

À l'étape suivante, vous allez intégrer des coroutines dans Room et Retrofit.

Pour poursuivre l'intégration des coroutines, nous allons utiliser la prise en charge des fonctions suspendues dans la version stable de Room et Retrofit, puis simplifier considérablement le code que nous venons d'écrire en utilisant les fonctions suspendues.

Coroutines dans Room

Commencez par ouvrir MainDatabase.kt et faites de insertTitle une fonction de suspension :

MainDatabase.kt

// add the suspend modifier to the existing insertTitle

@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)

Dans ce cas, Room rendra votre requête sécurisée pour le thread principal et l'exécutera automatiquement sur un thread d'arrière-plan. Toutefois, cela signifie également que vous ne pouvez appeler cette requête qu'à partir d'une coroutine.

Et c'est tout ce que vous avez à faire pour utiliser des coroutines dans Room. Plutôt pratique.

Coroutines dans Retrofit

Voyons maintenant comment intégrer des coroutines à Retrofit. Ouvrez MainNetwork.kt et remplacez fetchNextTitle par une fonction de suspension.

MainNetwork.kt

// add suspend modifier to the existing fetchNextTitle
// change return type from Call<String> to String

interface MainNetwork {
   @GET("next_title.json")
   suspend fun fetchNextTitle(): String
}

Pour utiliser des fonctions de suspension avec Retrofit, vous devez effectuer deux opérations :

  1. Ajouter un modificateur suspend à la fonction
  2. Supprimez le wrapper Call du type renvoyé. Ici, nous renvoyons String, mais vous pouvez également renvoyer un type complexe basé sur JSON. Si vous souhaitez toujours fournir un accès à l'intégralité de Result de Retrofit, vous pouvez renvoyer Result<String> au lieu de String à partir de la fonction suspendue.

Retrofit rendra automatiquement les fonctions de suspension main-safe afin que vous puissiez les appeler directement depuis Dispatchers.Main.

Utiliser Room et Retrofit

Maintenant que Room et Retrofit sont compatibles avec les fonctions de suspension, nous pouvons les utiliser à partir de notre dépôt. Ouvrez TitleRepository.kt et voyez comment l'utilisation de fonctions de suspension simplifie considérablement la logique, même par rapport à la version de blocage :

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Waouh, c'est beaucoup plus court. Que s'est-il passé ? Il s'avère que s'appuyer sur la suspension et la reprise permet de raccourcir considérablement le code. Retrofit nous permet d'utiliser des types de retour tels que String ou un objet User ici, au lieu d'un Call. Cette opération est sûre, car à l'intérieur de la fonction de suspension, Retrofit peut exécuter la requête réseau sur un thread d'arrière-plan et reprendre la coroutine lorsque l'appel est terminé.

Mieux encore, nous avons supprimé le withContext. Étant donné que Room et Retrofit fournissent des fonctions de suspension sécurisées, il est possible d'orchestrer ce travail asynchrone à partir de Dispatchers.Main.

Corriger les erreurs de compilation

Le passage aux coroutines implique de modifier la signature des fonctions, car vous ne pouvez pas appeler une fonction de suspension à partir d'une fonction standard. Lorsque vous avez ajouté le modificateur suspend à cette étape, quelques erreurs de compilation ont été générées pour montrer ce qui se passerait si vous transformiez une fonction en fonction de suspension dans un projet réel.

Parcourez le projet et corrigez les erreurs du compilateur en modifiant la fonction pour qu'elle soit suspendue. Voici les solutions rapides pour chaque problème :

TestingFakes.kt

Mettez à jour les faux tests pour prendre en charge les nouveaux modificateurs de suspension.

TitleDaoFake

  1. Appuyez sur Alt+Entrée pour ajouter des modificateurs de suspension à toutes les fonctions de la hiérarchie.

MainNetworkFake

  1. Appuyez sur Alt+Entrée pour ajouter des modificateurs de suspension à toutes les fonctions de la hiérarchie.
  2. Remplacez fetchNextTitle par cette fonction.
override suspend fun fetchNextTitle() = result

MainNetworkCompletableFake

  1. Appuyez sur Alt+Entrée pour ajouter des modificateurs de suspension à toutes les fonctions de la hiérarchie.
  2. Remplacez fetchNextTitle par cette fonction.
override suspend fun fetchNextTitle() = completable.await()

TitleRepository.kt

  • Supprimez la fonction refreshTitleWithCallbacks, car elle n'est plus utilisée.

Exécuter l'application

Exécutez à nouveau l'application. Une fois compilée, vous verrez qu'elle charge les données à l'aide de coroutines, du ViewModel à Room et Retrofit.

Félicitations, vous avez complètement remplacé les threads par des coroutines dans cette application ! Pour conclure, nous verrons comment tester ce que nous venons de faire.

Dans cet exercice, vous allez écrire un test qui appelle directement une fonction suspend.

Comme refreshTitle est exposé en tant qu'API publique, il sera testé directement, ce qui montrera comment appeler des fonctions de coroutines à partir de tests.

Voici la fonction refreshTitle que vous avez implémentée dans le dernier exercice :

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = network.fetchNextTitle()
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Écrire un test qui appelle une fonction de suspension

Ouvrez TitleRepositoryTest.kt dans le dossier test, qui contient deux tâches à faire.

Essayez d'appeler refreshTitle à partir du premier whenRefreshTitleSuccess_insertsRows de test.

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   subject.refreshTitle()
}

Comme refreshTitle est une fonction suspend, Kotlin ne sait pas comment l'appeler, sauf à partir d'une coroutine ou d'une autre fonction de suspension. Vous obtiendrez une erreur de compilation telle que "La fonction de suspension refreshTitle ne doit être appelée qu'à partir d'une coroutine ou d'une autre fonction de suspension."

Le test runner ne connaît rien des coroutines. Nous ne pouvons donc pas faire de ce test une fonction de suspension. Nous pourrions launch une coroutine à l'aide d'un CoroutineScope comme dans un ViewModel, mais les tests doivent exécuter les coroutines jusqu'à la fin avant de renvoyer un résultat. Une fois qu'une fonction de test renvoie une valeur, le test est terminé. Les coroutines démarrées avec launch sont du code asynchrone, qui peut se terminer à un moment donné dans le futur. Par conséquent, pour tester ce code asynchrone, vous avez besoin d'un moyen de demander au test d'attendre la fin de votre coroutine. Comme launch est un appel non bloquant, cela signifie qu'il renvoie immédiatement et peut continuer à exécuter une coroutine après le retour de la fonction. Il ne peut pas être utilisé dans les tests. Exemple :

@Test
fun whenRefreshTitleSuccess_insertsRows() {
   val subject = TitleRepository(
       MainNetworkFake("OK"),
       TitleDaoFake("title")
   )

   // launch starts a coroutine then immediately returns
   GlobalScope.launch {
       // since this is asynchronous code, this may be called *after* the test completes
       subject.refreshTitle()
   }
   // test function returns immediately, and
   // doesn't see the results of refreshTitle
}

Ce test échouera parfois. L'appel à launch renverra immédiatement une valeur et s'exécutera en même temps que le reste du cas de test. Le test n'a aucun moyen de savoir si refreshTitle a déjà été exécuté ou non. Toute assertion, comme la vérification de la mise à jour de la base de données, serait instable. De plus, si refreshTitle a généré une exception, elle ne sera pas générée dans la pile d'appels de test. Il sera plutôt transmis au gestionnaire d'exceptions non interceptées de GlobalScope.

La bibliothèque kotlinx-coroutines-test contient la fonction runBlockingTest qui se bloque lorsqu'elle appelle des fonctions de suspension. Lorsque runBlockingTest appelle une fonction de suspension ou launches une nouvelle coroutine, il l'exécute immédiatement par défaut. Vous pouvez y voir un moyen de convertir des fonctions de suspension et des coroutines en appels de fonction normaux.

De plus, runBlockingTest relancera les exceptions non interceptées pour vous. Cela facilite les tests lorsqu'une coroutine génère une exception.

Implémenter un test avec une coroutine

Encapsulez l'appel à refreshTitle avec runBlockingTest et supprimez l'encapsuleur GlobalScope.launch de subject.refreshTitle().

TitleRepositoryTest.kt

@Test
fun whenRefreshTitleSuccess_insertsRows() = runBlockingTest {
   val titleDao = TitleDaoFake("title")
   val subject = TitleRepository(
           MainNetworkFake("OK"),
           titleDao
   )

   subject.refreshTitle()
   Truth.assertThat(titleDao.nextInsertedOrNull()).isEqualTo("OK")
}

Ce test utilise les faux fournis pour vérifier que "OK" est inséré dans la base de données par refreshTitle.

Lorsque le test appelle runBlockingTest, il se bloque jusqu'à ce que la coroutine lancée par runBlockingTest soit terminée. Ensuite, à l'intérieur, lorsque nous appelons refreshTitle, il utilise le mécanisme de suspension et de reprise habituel pour attendre que la ligne de base de données soit ajoutée à notre faux.

Une fois la coroutine de test terminée, runBlockingTest est renvoyé.

Écrire un test de délai d'attente

Nous souhaitons ajouter un court délai avant expiration à la requête réseau. Commençons par écrire le test, puis implémentons le délai avant expiration. Créez un test :

TitleRepositoryTest.kt

@Test(expected = TitleRefreshError::class)
fun whenRefreshTitleTimeout_throws() = runBlockingTest {
   val network = MainNetworkCompletableFake()
   val subject = TitleRepository(
           network,
           TitleDaoFake("title")
   )

   launch {
       subject.refreshTitle()
   }

   advanceTimeBy(5_000)
}

Ce test utilise le faux MainNetworkCompletableFake fourni, qui est un faux réseau conçu pour suspendre les appelants jusqu'à ce que le test les reprenne. Lorsque refreshTitle tente d'effectuer une requête réseau, il se bloque indéfiniment, car nous voulons tester les délais d'attente.

Ensuite, il lance une coroutine distincte pour appeler refreshTitle. Il s'agit d'un élément clé des tests de délais d'attente. Le délai d'attente doit se produire dans une coroutine différente de celle créée par runBlockingTest. Nous pouvons ainsi appeler la ligne suivante, advanceTimeBy(5_000), qui fera avancer le temps de cinq secondes et entraînera l'expiration de l'autre coroutine.

Il s'agit d'un test complet de délai d'inactivité, qui sera réussi une fois que nous aurons implémenté le délai d'inactivité.

Exécutez-le maintenant et voyez ce qui se passe :

Caused by: kotlinx.coroutines.test.UncompletedCoroutinesError: Test finished with active jobs: ["...]

L'une des fonctionnalités de runBlockingTest est qu'il ne vous permet pas de laisser fuir les coroutines une fois le test terminé. Si des coroutines ne sont pas terminées à la fin du test, comme notre coroutine de lancement, le test échouera.

Ajouter un délai d'inactivité

Ouvrez TitleRepository et ajoutez un délai d'expiration de cinq secondes à la récupération du réseau. Pour ce faire, utilisez la fonction withTimeout :

TitleRepository.kt

suspend fun refreshTitle() {
   try {
       // Make network request using a blocking call
       val result = withTimeout(5_000) {
           network.fetchNextTitle()
       }
       titleDao.insertTitle(Title(result))
   } catch (cause: Throwable) {
       // If anything throws an exception, inform the caller
       throw TitleRefreshError("Unable to refresh title", cause)
   }
}

Exécutez le test. Lorsque vous exécutez les tests, vous devriez voir que tous réussissent.

Dans le prochain exercice, vous apprendrez à écrire des fonctions d'ordre supérieur à l'aide de coroutines.

Dans cet exercice, vous allez refactoriser refreshTitle dans MainViewModel pour utiliser une fonction de chargement de données générale. Vous apprendrez à créer des fonctions d'ordre supérieur qui utilisent des coroutines.

L'implémentation actuelle de refreshTitle fonctionne, mais nous pouvons créer une coroutine de chargement de données générale qui affiche toujours le spinner. Cela peut être utile dans une base de code qui charge des données en réponse à plusieurs événements et qui souhaite s'assurer que le spinner de chargement est affiché de manière cohérente.

En examinant l'implémentation actuelle, chaque ligne, à l'exception de repository.refreshTitle(), est un code passe-partout permettant d'afficher le spinner et les erreurs.

// MainViewModel.kt

fun refreshTitle() {
   viewModelScope.launch {
       try {
           _spinner.value = true
           // this is the only part that changes between sources
           repository.refreshTitle() 
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Utiliser des coroutines dans des fonctions d'ordre supérieur

Ajoutez ce code à MainViewModel.kt.

MainViewModel.kt

private fun launchDataLoad(block: suspend () -> Unit): Job {
   return viewModelScope.launch {
       try {
           _spinner.value = true
           block()
       } catch (error: TitleRefreshError) {
           _snackBar.value = error.message
       } finally {
           _spinner.value = false
       }
   }
}

Refactorisez maintenant refreshTitle() pour utiliser cette fonction d'ordre supérieur.

MainViewModel.kt

fun refreshTitle() {
   launchDataLoad {
       repository.refreshTitle()
   }
}

En faisant abstraction de la logique d'affichage d'une icône de chargement et des erreurs, nous avons simplifié le code nécessaire au chargement des données. Afficher un indicateur de progression ou une erreur est facile à généraliser pour tout chargement de données, tandis que la source et la destination des données réelles doivent être spécifiées à chaque fois.

Pour créer cette abstraction, launchDataLoad prend un argument block qui est un lambda suspendu. Un lambda de suspension vous permet d'appeler des fonctions de suspension. C'est ainsi que Kotlin implémente les outils de création de coroutines launch et runBlocking que nous avons utilisés dans cet atelier de programmation.

// suspend lambda

block: suspend () -> Unit

Pour créer un lambda suspendu, commencez par le mot clé suspend. La flèche de fonction et le type de retour Unit complètent la déclaration.

Vous n'avez pas souvent besoin de déclarer vos propres lambdas de suspension, mais ils peuvent être utiles pour créer des abstractions comme celle-ci qui encapsulent une logique répétée.

Dans cet exercice, vous allez apprendre à utiliser du code basé sur des coroutines à partir de WorkManager.

Qu'est-ce que WorkManager ?

De nombreuses options sont disponibles sur Android pour un travail en arrière-plan différable. Cet exercice vous montre comment intégrer WorkManager aux coroutines. WorkManager est une bibliothèque rétrocompatible, flexible et simple pour exécuter en arrière-plan des travaux différables. WorkManager est la solution recommandée pour ces cas d'utilisation sur Android.

WorkManager fait partie d'Android Jetpack et est un composant d'architecture pour les tâches en arrière-plan qui nécessitent une combinaison d'exécution opportuniste et garantie. Une exécution opportuniste signifie que WorkManager exécute le travail en arrière-plan dès que possible. Une exécution garantie signifie que WorkManager gère la logique permettant de démarrer le travail dans diverses situations, même si vous quittez votre application.

WorkManager est donc un bon choix pour les tâches qui doivent être effectuées à terme.

Voici quelques exemples de tâches pour lesquelles WorkManager s'avère particulièrement utile :

  • Envoi de journaux
  • Application de filtres à des images et enregistrement de celles-ci
  • Synchronisation périodique des données locales avec le réseau

Utiliser des coroutines avec WorkManager

WorkManager fournit différentes implémentations de sa classe de base ListanableWorker pour différents cas d'utilisation.

La classe Worker la plus simple nous permet d'exécuter une opération synchrone par WorkManager. Cependant, après avoir travaillé jusqu'à présent à convertir notre code base pour utiliser des coroutines et des fonctions de suspension, la meilleure façon d'utiliser WorkManager est d'utiliser la classe CoroutineWorker qui permet de définir notre fonction doWork() comme fonction de suspension.

Pour commencer, ouvrez RefreshMainDataWork. Il étend déjà CoroutineWorker et vous devez implémenter doWork.

Dans la fonction suspend doWork, appelez refreshTitle() à partir du dépôt et renvoyez le résultat approprié.

Une fois la tâche TODO terminée, le code se présente comme suit :

override suspend fun doWork(): Result {
   val database = getDatabase(applicationContext)
   val repository = TitleRepository(network, database.titleDao)

   return try {
       repository.refreshTitle()
       Result.success()
   } catch (error: TitleRefreshError) {
       Result.failure()
   }
}

Notez que CoroutineWorker.doWork() est une fonction de suspension. Contrairement à la classe Worker plus simple, ce code ne s'exécute PAS sur l'exécuteur spécifié dans votre configuration WorkManager, mais utilise plutôt le répartiteur dans le membre coroutineContext (par défaut Dispatchers.Default).

Tester notre CoroutineWorker

Aucune base de code ne devrait être complète sans tests.

WorkManager propose plusieurs façons de tester vos classes Worker. Pour en savoir plus sur l'infrastructure de test d'origine, consultez la documentation.

WorkManager v2.1 introduit un nouvel ensemble d'API pour simplifier le test des classes ListenableWorker et, par conséquent, de CoroutineWorker. Dans notre code, nous allons utiliser l'une de ces nouvelles API : TestListenableWorkerBuilder.

Pour ajouter notre nouveau test, mettez à jour le fichier RefreshMainDataWorkTest dans le dossier androidTest.

Le contenu du fichier est le suivant :

package com.example.android.kotlincoroutines.main

import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import android.content.Context
import androidx.test.core.app.ApplicationProvider
import androidx.work.ListenableWorker.Result
import androidx.work.testing.TestListenableWorkerBuilder
import com.example.android.kotlincoroutines.fakes.MainNetworkFake
import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4


@RunWith(JUnit4::class)
class RefreshMainDataWorkTest {

@Test
fun testRefreshMainDataWork() {
   val fakeNetwork = MainNetworkFake("OK")

   val context = ApplicationProvider.getApplicationContext<Context>()
   val worker = TestListenableWorkerBuilder<RefreshMainDataWork>(context)
           .setWorkerFactory(RefreshMainDataWork.Factory(fakeNetwork))
           .build()

   // Start the work synchronously
   val result = worker.startWork().get()

   assertThat(result).isEqualTo(Result.success())
}

}

Avant de passer au test, nous indiquons à WorkManager l'usine afin de pouvoir injecter le faux réseau.

Le test lui-même utilise TestListenableWorkerBuilder pour créer notre nœud de calcul, que nous pouvons ensuite exécuter en appelant la méthode startWork().

WorkManager n'est qu'un exemple de la façon dont les coroutines peuvent être utilisées pour simplifier la conception des API.

Dans cet atelier de programmation, nous avons abordé les bases dont vous aurez besoin pour commencer à utiliser des coroutines dans votre application.

Nous avons abordé les points suivants :

  • Comment intégrer des coroutines aux applications Android à partir des tâches d'UI et WorkManager pour simplifier la programmation asynchrone
  • Utiliser des coroutines dans un ViewModel pour récupérer des données du réseau et les enregistrer dans une base de données sans bloquer le thread principal.
  • et comment annuler toutes les coroutines lorsque le ViewModel est terminé.

Pour tester le code basé sur les coroutines, nous avons abordé les deux aspects en testant le comportement et en appelant directement les fonctions suspend à partir des tests.

En savoir plus

Consultez l'atelier de programmation Coroutines avancées avec Kotlin Flow et LiveData pour en savoir plus sur l'utilisation avancée des coroutines sur Android.

Les coroutines Kotlin comportent de nombreuses fonctionnalités qui n'ont pas été abordées dans cet atelier de programmation. Si vous souhaitez en savoir plus sur les coroutines Kotlin, consultez les guides sur les coroutines publiés par JetBrains. Consultez également Améliorer les performances des applications avec les coroutines Kotlin pour découvrir d'autres modèles d'utilisation des coroutines sur Android.