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
etRoom
- 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
etrunBlocking
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 :
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 :
- Si vous avez téléchargé le fichier ZIP
kotlin-coroutines
, décompressez-le. - Ouvrez le projet
coroutines-codelab
dans Android Studio. - Sélectionnez le module d'application
start
. - Cliquez sur le bouton
Exé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.
MainActivity
affiche l'UI, enregistre les écouteurs de clics et peut afficher unSnackbar
. Il transmet les événements àMainViewModel
et met à jour l'écran en fonction deLiveData
dansMainViewModel
.MainViewModel
gère les événements dansonMainViewClicked
et communique avecMainActivity
à l'aide deLiveData.
.Executors
définitBACKGROUND,
, qui peut exécuter des éléments sur un thread en arrière-plan.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 :
viewModelScope.
launch
démarrera une coroutine dansviewModelScope
. 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 dedelay
, cette coroutine sera automatiquement annulée lorsqueonCleared
sera appelé lors de la destruction du ViewModel.- Comme
viewModelScope
a un répartiteur par défaut deDispatchers.Main
, cette coroutine sera lancée dans le thread principal. Nous verrons plus tard comment utiliser différents threads. - La fonction
delay
est une fonctionsuspend
. Dans Android Studio, cela est indiqué par l'icônedans 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 :
InstantTaskExecutorRule
est une règle JUnit qui configureLiveData
pour exécuter chaque tâche de manière synchrone.MainCoroutineScopeRule
est une règle personnalisée dans cette codebase qui configureDispatchers.Main
pour utiliser unTestCoroutineDispatcher
à partir dekotlinx-coroutines-test
. Cela permet aux tests de faire avancer une horloge virtuelle pour les tests et au code d'utiliserDispatchers.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
- Effectuez un clic droit sur le nom de la classe
MainViewModelTest
dans votre éditeur pour ouvrir un menu contextuel. - Dans le menu contextuel, sélectionnez
Exécuter "MainViewModelTest".
- Pour les exécutions ultérieures, vous pouvez sélectionner cette configuration de test dans les configurations à côté du bouton
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.
MainDatabase
implémente une base de données à l'aide de Room qui enregistre et charge unTitle
.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.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.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 sur. 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.
- Passer à un autre fil de discussion avec
BACKGROUND
ExecutorService
- Exécutez la requête réseau
fetchNextTitle
à l'aide de la méthode de blocageexecute()
. Cela exécutera la requête réseau dans le thread actuel, dans ce cas, l'un des threads deBACKGROUND
. - Si le résultat est positif, enregistrez-le dans la base de données avec
insertTitle
et appelez la méthodeonCompleted()
. - 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 :
withContext
renvoie son résultat au Dispatcher qui l'a appelé, en l'occurrenceDispatchers.Main
. La version de rappel appelait les rappels sur un thread dans le service d'exécutionBACKGROUND
.- 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 :
- Ajouter un modificateur suspend à la fonction
- Supprimez le wrapper
Call
du type renvoyé. Ici, nous renvoyonsString
, mais vous pouvez également renvoyer un type complexe basé sur JSON. Si vous souhaitez toujours fournir un accès à l'intégralité deResult
de Retrofit, vous pouvez renvoyerResult<String>
au lieu deString
à 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
- Appuyez sur Alt+Entrée pour ajouter des modificateurs de suspension à toutes les fonctions de la hiérarchie.
MainNetworkFake
- Appuyez sur Alt+Entrée pour ajouter des modificateurs de suspension à toutes les fonctions de la hiérarchie.
- Remplacez
fetchNextTitle
par cette fonction.
override suspend fun fetchNextTitle() = result
MainNetworkCompletableFake
- Appuyez sur Alt+Entrée pour ajouter des modificateurs de suspension à toutes les fonctions de la hiérarchie.
- 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.