Dans cet atelier de programmation, vous allez apprendre à utiliser des coroutines Kotlin dans une application Android, une nouvelle manière de gérer les threads en arrière-plan qui peuvent simplifier le code en réduisant le recours aux rappels. Les coroutines sont une fonctionnalité Kotlin qui convertit les rappels asynchrones pour des tâches de longue durée, telles que l'accès à une base de données ou au 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 le rappel 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 de 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 les coroutines dans votre application afin de charger des données à partir du réseau. Vous pourrez également intégrer des coroutines dans une application. Vous connaîtrez également les bonnes pratiques en matière de coroutines et apprendrez à écrire un test sur du code qui utilise des coroutines.
Conditions préalables
- 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
- Code d'appel écrit avec des coroutines et permettant d'obtenir des résultats.
- Utiliser des fonctions de suspension 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
. - Utiliser des coroutines avec des composants d'architecture
- Découvrez les bonnes pratiques pour tester les coroutines.
Ce dont vous avez besoin
- Android Studio 3.5 (l'atelier de programmation peut fonctionner avec d'autres versions, mais il se peut que certaines données soient manquantes ou différentes)
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
Run (Exécuter), puis choisissez un émulateur ou connectez votre appareil Android, qui doit pouvoir exécuter Android Lollipop (SDK 21 au minimum). L'écran Kotlin Coroutines doit s'afficher:
Cette application de départ utilise les fils de discussion pour augmenter progressivement le nombre de fois où vous avez appuyé sur l'écran. Il récupère également un nouveau titre du réseau et l'affiche à l'écran. Essayez dès 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 utiliser des coroutines.
Cette application utilise des composants d'architecture pour séparer le code de l'interface utilisateur dans MainActivity
de la logique d'application dans MainViewModel
. Prenez quelques instants pour vous familiariser avec la structure du projet.
MainActivity
affiche l'interface utilisateur, 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 communiquera avecMainActivity
à l'aide deLiveData.
Executors
définitBACKGROUND,
qui peut exécuter des éléments dans un thread d'arrière-plan.TitleRepository
récupère les résultats depuis le réseau et les enregistre dans la base de données.
Ajouter des coroutines à un projet
Pour utiliser les coroutines en langage 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 le font déjà pour vous. Vous n'avez donc pas besoin de le faire pour terminer l'atelier de programmation.
Les coroutines sur Android sont disponibles en tant que bibliothèque principale et les extensions spécifiques à Android:
- kotlinx-corountines-core : interface principale pour utiliser des coroutines en langage Kotlin
- kotlinx-coroutines-android : compatibilité avec le thread Android principal dans les coroutines
L'application de départ inclut déjà les dépendances de l'élément build.gradle.
. Lorsque vous créez un projet d'application, vous devez ouvrir build.gradle (Module: app)
et ajouter les dépendances de 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 aussi le thread qui appelle tous les gestionnaires de clics et autres rappels d'interface utilisateur. Elle doit donc fonctionner en douceur pour garantir une expérience utilisateur optimale.
Pour que l'application soit visible par l'utilisateur sans aucune mise en pause visible, le thread principal doit mettre à jour l'écran toutes les 16 ms ou plus, soit environ 60 images par seconde. La plupart des tâches courantes prennent plus de temps, par exemple, l'analyse d'ensembles de données JSON volumineux, l'écriture de données dans une base de données ou l'extraction de données à partir du réseau. Par conséquent, l'appel de ce type de code à partir du thread principal peut entraîner la mise en veille, la stuttering ou même le blocage de l'application. Si vous bloquez le thread principal trop longtemps, l'application risque de planter et d'afficher la 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 présentant la sécurité principale.
Schéma de rappel
Les rappels permettent d'effectuer des tâches de longue durée sans bloquer le thread principal. Les rappels vous permettent de démarrer des tâches de longue durée dans un thread en arrière-plan. Une fois la tâche terminée, le rappel est appelé pour vous informer du résultat dans le thread principal.
Examinez un exemple de schéma 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 suffisamment rapidement pour s'exécuter sur le thread principal. Elle doit donc se renvoyer très rapidement, afin que la prochaine mise à jour de l'écran ne soit pas retardée. Toutefois, comme le fichier slowFetch
prendra quelques secondes, voire des minutes, le fil de discussion principal ne peut pas attendre le résultat. Le rappel show(result)
permet à slowFetch
de s'exécuter sur un thread en arrière-plan et de renvoyer le résultat lorsqu'il est prêt.
Supprimer des rappels à l'aide de coroutines
Les rappels sont un excellent modèle, mais ils présentent quelques inconvénients. Le code qui utilise beaucoup les rappels peut s'avérer difficile à lire et à raison. De plus, les rappels ne permettent pas d'utiliser certaines fonctionnalités linguistiques, comme les exceptions.
Les coroutines Kotlin vous permettent de convertir du code basé sur le rappel en code séquentiel. Le code écrit de façon séquentielle est généralement plus facile à lire et peut même utiliser des fonctionnalités linguistiques telles que des exceptions.
Au final, ils font exactement la même chose: attendre qu'un résultat soit disponible à partir d'une tâche de longue durée et poursuivre l'exécution. Cependant, dans leur code, elles sont très différentes.
Le mot clé suspend
est une méthode Kotlin' permettant de marquer une fonction, ou un type de fonction, disponible pour les coroutines. Lorsqu'une coroutine appelle une fonction marquée comme suspend
, au lieu de la bloquer jusqu'à ce qu'elle affiche un appel de fonction normal, elle suspendre l'exécution jusqu'à ce que le résultat soit prêt, puis elle reprene là où elle s'était terminée. Pendant qu'il est suspendu en attendant un résultat, il débloque le thread sur lequel il 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 deux fonctions de 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 { ... }
Tout comme avec la version de rappel, makeNetworkRequest
doit renvoyer immédiatement le fil de discussion principal, car il porte la valeur @UiThread
. Cela signifie qu'il ne parvient généralement pas à appeler des méthodes de blocage telles que slowFetch
. C'est ici que le mot clé suspend
opère sa magie.
Par rapport au code basé sur le rappel, le code de coroutine offre le même résultat obtenu par le 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 de rappels. Par exemple, le code qui récupère un résultat à partir de deux points de terminaison du réseau et l'enregistre dans la base de données peut être écrit en tant que fonction dans des coroutines sans rappel. Comme ceci:
// 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 { ... }
Vous présenterez des coroutines dans l'exemple d'application dans la section suivante.
Dans cet exercice, vous allez écrire une coroutine pour afficher un message après un délai. Pour commencer, assurez-vous d'avoir ouvert le module start
dans Android Studio.
Comprendre CoroutineScope
Dans Kotlin, toutes les coroutines s'exécutent dans un élément 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. Sous Android, vous pouvez utiliser un champ d'application pour annuler toutes les coroutines en cours d'exécution lorsque, par exemple, l'utilisateur quitte une Activity
ou une Fragment
. Les champs d'application vous permettent également de spécifier un coordinateur par défaut. Un coordinateur contrôle quel thread exécute une coroutine.
Pour les coroutines démarrées par l'UI, il est généralement correct de les lancer sur Dispatchers.Main
, le thread principal sur Android. Une coroutine démarrée sur Dispatchers.Main
ne bloque pas le thread principal pendant la suspension. Étant donné qu'une coroutine ViewModel
met presque toujours à jour l'interface utilisateur du thread principal, le démarrage de coroutines sur le thread principal vous permet d'économiser des commutateurs supplémentaires. Une coroutine démarrée sur le thread principal peut changer de coordinateur à tout moment après son démarrage. Par exemple, il peut utiliser un autre coordinateur pour analyser un résultat JSON volumineux à partir du thread principal.
Utiliser viewModelScope
La bibliothèque AndroidX lifecycle-viewmodel-ktx
ajoute un CoroutineScope à ViewModel qui est configuré pour démarrer des coroutines liées à l'interface utilisateur. Pour utiliser cette bibliothèque, vous devez l'inclure dans le fichier build.gradle (Module: start)
de votre projet. Cette étape est déjà 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 associé à Dispatchers.Main
et sera automatiquement annulé lorsque ViewModel
sera effacé.
Passer des fils de discussion aux coroutines
Dans MainViewModel.kt
, recherchez le texte"TODO" (À FAIRE) avec le code suivant:
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 en arrière-plan. Comme sleep
bloque le thread actuel, il figera l'interface utilisateur s'il est appelé sur le thread principal. Une seconde après que l'utilisateur a cliqué sur la vue principale, un snack-bar est demandé.
Vous pouvez le constater en supprimant le BACKGROUND du code, puis en l'exécutant à nouveau. L'icône de chargement ne s'affiche pas, et tout passe à 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 une 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 de même, car il attend une seconde avant d'afficher un snack-bar. Toutefois, il existe des différences importantes:
viewModelScope.
launch
va démarrer une coroutine dans leviewModelScope
. Cela signifie que lorsque la tâche que nous avons transmise àviewModelScope
est annulée, toutes les coroutines de cette tâche/portée sont annulées. Si l'utilisateur a quitté l'activité avant le retour dedelay
, cette coroutine est automatiquement annulée lorsqueonCleared
est appelé lors de la destruction de ViewModel.viewModelScope
ayant le coordinateurDispatchers.Main
par défaut, cette coroutine sera lancée dans le thread principal. Nous verrons plus tard comment utiliser différents fils de discussion.- La fonction
delay
est une fonctionsuspend
. Dans Android Studio, cette icône est représentée par l'icônedans la marge de gauche. Même si cette coroutine s'exécute sur le thread principal,
delay
ne bloque pas le thread pendant une seconde. À la place, le coordinateur planifie la reprise de la coroutine dans l'ordre suivant.
Exécutez la commande. Lorsque vous cliquez sur la vue principale, un snack-bar doit 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 exécutées sur Dispatchers.Main
à l'aide de la bibliothèque kotlinx-coroutines-test. Plus tard dans cet atelier de programmation, vous allez implémenter un test qui interagit directement avec les coroutines.
Vérifier 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 en dehors de l'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 de ce codebase qui configureDispatchers.Main
pour utiliser unTestCoroutineDispatcher
à partir dekotlinx-coroutines-test
. Cela permet aux tests d'avancer une horloge virtuelle pour le test, et au code d'utiliserDispatchers.Main
dans les tests unitaires.
Dans la méthode setup
, une instance de MainViewModel
est créée à l'aide de contrefaçons test. Il s'agit de fausses implémentations de réseau et de base de données dans le code de démarrage pour faciliter l'écriture de tests sans utiliser le réseau ou la base de données réels.
Pour ce test, les falsifications ne sont nécessaires que pour satisfaire les dépendances de MainViewModel
. Plus tard dans cet atelier de programmation, vous allez mettre à jour les contrefaçons pour prendre en charge les coroutines.
Écrire un test qui contrôle les coroutines
Ajoutez un test pour vérifier que les appuis sont mis à jour une seconde après le clic 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 des appuis reste "0 appui " juste après que onMainViewClicked
a été appelé, puis une seconde plus tard, est mis à jour et affiche 1 appuis ".
Ce test utilise le temps virtuel 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 le Dispatchers.Main
. Ici, nous appelons advanceTimeBy(1_000)
, ce qui entraîne l'exécution immédiate des coroutines par le coordinateur principal, qui seront relancées une seconde plus tard.
Ce test est entièrement déterministe, ce qui signifie qu'il fonctionnera toujours de la même manière. Et parce qu'elle dispose d'un contrôle total sur l'exécution des coroutines lancées sur le Dispatchers.Main
, elle n'a pas besoin d'attendre une seconde que la valeur soit définie.
Exécuter le test existant
- Effectuez un clic droit sur le nom du cours
MainViewModelTest
dans votre éditeur pour ouvrir un menu contextuel. - Dans le menu contextuel, sélectionnez
Run 'MainViewModelTest'.
- Pour les prochaines exécutions, vous pourrez sélectionner cette configuration de test dans les configurations à côté du bouton
de la barre d'outils. Par défaut, la configuration est appelée MainViewModelTest.
Vous devriez voir la réussite de l'examen. L'exécution devrait prendre moins d'une seconde.
Dans l'exercice suivant, vous apprendrez à convertir des API de rappel existantes en coroutines.
Au cours de cette étape, vous allez commencer à convertir un dépôt pour utiliser des coroutines. Pour ce faire, nous allons ajouter des coroutines aux éléments ViewModel
, Repository
, Room
et Retrofit
.
Nous vous recommandons de comprendre de quoi est responsable chaque partie de l'architecture avant de passer à l'utilisation de coroutines.
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 simulations de données de manière aléatoire, mais se comporte comme s'il s'agissait de requêtes réseau réelles.TitleRepository
implémente une API unique pour extraire 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. Elle indiquera au dépôt d'actualiser le titre quand l'utilisateur appuiera sur l'écran.
Comme la requête réseau est pilotée par les événements d'interface utilisateur et que nous voulons démarrer une coroutine en fonction de ces événements, l'endroit naturel pour commencer à utiliser des coroutines se trouve dans le ViewModel
.
Version du 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 provoque l'actualisation du titre et l'écriture du nouveau titre dans la base de données.
Cette implémentation utilise un rappel pour effectuer les opérations suivantes:
- Avant de lancer une requête, elle affiche une icône de chargement avec
_spinner.value = true
. - Lorsqu'elle obtient un résultat, elle efface l'icône de chargement avec
_spinner.value = false
- Si une erreur se produit, un snack-bar s'affiche et l'icône s'efface.
Notez que le rappel onCompleted
n'est pas transmis à la méthode title
. Étant donné que nous écrivons tous les titres de la base de données Room
, l'interface utilisateur est mise à jour vers le titre actuel en observant un LiveData
modifié par Room
.
Dans la mise à jour des coroutines, nous allons conserver exactement le même comportement. C'est un bon modèle d'utilisation d'une source de données observable comme une base de données Room
pour maintenir automatiquement à jour l'interface utilisateur.
Version des coroutines
Réécrivez refreshTitle
avec des coroutines.
Étant donné que nous en aurons besoin immédiatement, nous allons créer 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 des 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 le modifierez 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 lui suffit de faire 500 millisecondes pour se charger du travail.
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 pour mettre à jour le nombre d'appuis, commencez par lancer une nouvelle coroutine dans viewModelScope
. Vous utiliserez Dispatchers.Main
, ce qui est correct. Même si refreshTitle
envoie une requête réseau et une requête de base de données, il peut utiliser des coroutines pour exposer une interface sécurisée. Cela signifie qu'il pourra être appelé en toute sécurité depuis le thread principal.
Étant donné que nous utilisons viewModelScope
, lorsque l'utilisateur quitte cet écran, le travail démarré par cette coroutine sera automatiquement annulé. Cela signifie qu'il n'effectue pas de requêtes réseau ou de requêtes de base de données supplémentaires.
Les quelques lignes de code suivantes appellent refreshTitle
dans repository
.
try {
_spinner.value = true
repository.refreshTitle()
}
Avant que cette coroutine ne fasse quoi que ce soit, elle démarre l'icône de chargement, puis appelle refreshTitle
comme une fonction normale. Cependant, comme refreshTitle
est une fonction de suspension, elle s'exécute différemment d'une fonction normale.
Nous n'avons pas besoin de transmettre un rappel. La coroutine sera suspendue jusqu'à ce qu'elle soit réactivée par refreshTitle
. Bien qu'il ressemble à un appel de fonction de blocage standard, il attend automatiquement que la requête sur le réseau et la base de données soit terminée avant de reprendre sans bloquer le thread principal.
} catch (error: TitleRefreshError) {
_snackBar.value = error.message
} finally {
_spinner.value = false
}
Les exceptions des fonctions de suspension fonctionnent comme les erreurs des fonctions standards. Si une erreur se produit dans une fonction de suspension, elle est renvoyée à l'appelant. Même s'ils s'exécutent assez différemment, vous pouvez utiliser des blocs try/cat standard pour les gérer. C'est utile, car cela vous permet de vous appuyer sur la compatibilité intégrée de la langue pour la gestion des erreurs au lieu de créer une gestion des erreurs personnalisée pour chaque rappel.
De même, si vous générez une exception à partir d'une coroutine, celle-ci annule par défaut le parent de celle-ci. Vous pouvez donc facilement annuler plusieurs tâches associées en même temps.
Ensuite, dans un dernier bloc, nous pouvons nous assurer que l'icône de chargement est toujours désactivée une fois la requête exécutée.
Exécutez à nouveau l'application en sélectionnant la configuration start, puis en appuyant sur . Vous devriez voir une icône de chargement lorsque vous appuyez n'importe où. Le titre ne change pas, car nous n'avons pas encore connecté notre réseau ou notre base de données.
Lors de l'exercice suivant, vous allez mettre à jour le dépôt pour qu'il fonctionne.
Dans cet exercice, vous allez apprendre à modifier le thread sur lequel une coroutine s'exécute afin de mettre en œuvre une version fonctionnelle de TitleRepository
.
Examiner le code de rappel existant dans refreshTitle
Ouvrez TitleRepository.kt
et examinez l'implémentation basée sur le rappel existante.
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 de nombreuses opérations pour implémenter l'actualisation.
- Basculer vers un autre fil de discussion avec
BACKGROUND
ExecutorService
- Exécutez la requête réseau
fetchNextTitle
à l'aide de la méthode bloquantexecute()
. La requête réseau sera exécutée dans le thread actuel, en l'occurrence l'un des threads deBACKGROUND
. - Si le résultat aboutit, enregistrez-le dans la base de données à l'aide de
insertTitle
, puis appelez la méthodeonCompleted()
. - Si le résultat a échoué ou qu'il existe une exception, appelez la méthode onError pour informer l'appelant de l'échec de l'actualisation.
Cette mise en œuvre basée sur le rappel est sécurisée, car elle ne bloque pas le thread principal. Toutefois, il doit utiliser un rappel pour informer l'appelant une fois que le travail est terminé. Il appelle également les rappels sur le thread BACKGROUND
qu'il a lui-même modifié.
Appels bloquant des appels à partir de coroutines
Sans introduit de coroutines dans le réseau ou la base de données, nous pouvons rendre ce code sécurisé à l'aide de coroutines. Cela nous permettra de supprimer le rappel et de transmettre le résultat au fil de discussion qui l'a initialement appelé.
Vous pouvez utiliser ce modèle chaque fois que vous devez bloquer ou utiliser une utilisation intensive du processeur à partir d'une coroutine, par exemple pour trier et filtrer une longue liste ou lire un disque.
Pour passer d'un coordinateur à 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 coordinateurs : 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 configuration utilise le blocage des appels pour le réseau et la base de données, mais c'est un peu plus simple que la version de rappel.
Ce code utilise toujours les appels bloquants. Le fait d'appeler execute()
et insertTitle(...)
bloque tous les deux le thread dans lequel cette coroutine est exécutée. Toutefois, en passant à Dispatchers.IO
avec withContext
, nous bloquons l'un des threads du coordinateur d'E/S. La coroutine qui a appelé cette fonction, éventuellement exécutée sur Dispatchers.Main
, sera suspendue jusqu'à ce que le lambda withContext
soit terminé.
La version de rappel présente deux différences importantes:
withContext
renvoie le résultat au coordinateur qui l'a appelé. Dans le cas présent,Dispatchers.Main
. La version de rappel, appelée les rappels sur un thread, dans le service d'exécutionBACKGROUND
.- L'appelant n'a pas besoin de transmettre un rappel à cette fonction. Il peut s'appuyer sur la suspension et la réactivation 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 à partir 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 compatibilité des fonctions de suspension dans la version stable de Room et Retrofit, puis simplifier le code que nous venons d'écrire de manière significative à l'aide des fonctions de suspension.
Coroutines dans Room
Commencez par ouvrir MainDatabase.kt
et définissez insertTitle
comme fonction de suspension:
MainDatabase.kt
// add the suspend modifier to the existing insertTitle
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTitle(title: Title)
Lorsque c'est le cas, Room assure la sécurité de votre requête et l'exécute automatiquement dans un thread en arrière-plan. Cependant, 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 les coroutines dans Room. Plutôt bien.
Coroutines dans Retrofit
Voyons maintenant comment intégrer des coroutines avec 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 de suspension à la fonction
- Supprimez le wrapper
Call
du type renvoyé. Dans cet exemple, nous affichonsString
, mais vous pouvez également renvoyer un type complexe basé sur JSON. Si vous souhaitez toujours autoriser l'accès à l'élémentResult
complet, vous pouvez renvoyerResult<String>
au lieu deString
à partir de la fonction de suspension.
Avec Retrofit, les fonctions de suspension sont sécurisées automatiquement afin que vous puissiez les appeler directement depuis Dispatchers.Main
.
Utiliser les salles et moderniser
Maintenant que Room et Retrofit sont compatibles avec les fonctions de suspension, nous pouvons les utiliser depuis notre dépôt. Ouvrez TitleRepository.kt
et voyez comment l'utilisation des fonctions de suspension simplifie considérablement la logique, même par rapport à la version de blocage:
Title Repository.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 beaucoupplus court. Que s'est-il passé ? En effet, grâce au processus de suspension et de reprise, le code peut être beaucoup plus court. Le remarketing nous permet d'utiliser des types de retour tels que String
ou un objet User
ici, au lieu de Call
. C'est une opération sûre, car dans la fonction de suspension, Retrofit
peut exécuter la requête réseau sur un thread en arrière-plan et reprendre la coroutine lorsque l'appel se termine.
Mieux encore, nous avons éliminé l'withContext
. Étant donné que Room et Retrofit fournissent des fonctions de suspension sécurisées, il est possible d'orchestrer cette tâche asynchrone à partir de Dispatchers.Main
.
Corriger les erreurs de compilation
Le déplacement vers des 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. Celui-ci indique ce qui se passerait si vous modifiiez une fonction de façon à suspendre le projet.
Parcourez le projet et corrigez les erreurs de compilation en modifiant la fonction pour suspendre la création. Voici les solutions possibles pour chacun:
TestFakes.kt
Mettez à jour les contrefaçons de test pour prendre en charge les nouveaux modificateurs de suspension.
TitreDaoFake
- Appuyez sur Entrée+Alt pour ajouter des modificateurs de suspension à toutes les fonctions dans la lourde
MainNetworkFake
- Appuyez sur Entrée+Alt pour ajouter des modificateurs de suspension à toutes les fonctions dans la lourde
- Remplacer
fetchNextTitle
par cette fonction
override suspend fun fetchNextTitle() = result
MainNetworkCompleteableFake
- Appuyez sur Entrée+Alt pour ajouter des modificateurs de suspension à toutes les fonctions dans la lourde
- Remplacer
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 la compilation compilée, vous constaterez qu'elle charge des données à l'aide de coroutines, de ViewModel à Room et Retrofit !
Félicitations, vous avez complètement changé cette application pour utiliser des coroutines ! Pour conclure, nous allons voir comment tester les actions que nous venons d'effectuer.
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 montre 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 TODO.
Essayez d'appeler refreshTitle
à partir du premier test whenRefreshTitleSuccess_insertsRows
.
@Test
fun whenRefreshTitleSuccess_insertsRows() {
val subject = TitleRepository(
MainNetworkFake("OK"),
TitleDaoFake("title")
)
subject.refreshTitle()
}
Comme Kotlin 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 alors une erreur de compilation telle que "Suspend function refreshTitle ne doit être appelé qu'à partir d'une coroutine ou d'une autre fonction de suspension."
Le lanceur de test ne sait rien des coroutines et ne peut donc pas effectuer ce test de suspension. Nous pourrions launch
une coroutine à l'aide d'un CoroutineScope
comme dans un ViewModel
. Toutefois, les tests doivent exécuter les coroutines jusqu'à ce qu'elles soient renvoyées. Une fois qu'une fonction de retour est renvoyée, le test est terminé. Les coroutines démarrées avec launch
sont du code asynchrone, qui peut se terminer à un moment donné. Par conséquent, pour tester ce code asynchrone, vous devez demander à votre test d'attendre la fin de votre coroutine. Étant donné que launch
est un appel non bloquant, 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 de launch
reviendra immédiatement et s'exécutera en même temps que le reste du scénario de test. Le test n'a aucun moyen de savoir si refreshTitle
s'est déjà exécuté. Toute assertion comme vérifier que la base de données a été mise à jour serait flakey. Et si refreshTitle
a renvoyé une exception, il ne sera pas renvoyé dans la pile des appels de test. Elle sera renvoyée dans le gestionnaire d'exceptions non intercepté GlobalScope
.
La bibliothèque kotlinx-coroutines-test
possède la fonction runBlockingTest
qui se bloque lorsqu'elle appelle des fonctions de suspension. Lorsque runBlockingTest
appelle une fonction de suspension ou une nouvelle coroutine launches
, elle s'exécute immédiatement par défaut. Il s'agit d'une façon de convertir des fonctions de suspension et des coroutines en appels de fonction normaux.
De plus, runBlockingTest
renverra les exceptions non détectées pour vous. Il est ainsi plus facile de tester quand une coroutine génère une exception.
Implémenter un test avec une coroutine
Encapsulez l'appel à refreshTitle
avec runBlockingTest
et supprimez le wrapper 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 contrefaçons fournies 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 démarrée par runBlockingTest
se termine. Ensuite, lorsque nous appelons refreshTitle
, le mécanisme de suspension et de reprise standard est utilisé, car il attend que la ligne de la base de données soit ajoutée à notre falsification.
Une fois la coroutine de test terminée, runBlockingTest
s'affiche de nouveau.
Écrire un test de délai avant expiration
Nous souhaitons ajouter un délai d'inactivité court à la requête réseau. Écrivons d'abord le test, puis implémentez 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
, qui est un faux réseau conçu pour suspendre les appelants jusqu'à ce que le test se poursuive. Lorsque refreshTitle
tente d'envoyer une requête réseau, il se bloque indéfiniment, car nous voulons tester les délais avant expiration.
Ensuite, elle lance une coroutine distincte pour appeler refreshTitle
. Il s'agit d'un élément clé du test des délais avant expiration, qui doit se produire dans une coroutine différente de celle créée par runBlockingTest
. Cela nous permet d'appeler la ligne suivante, advanceTimeBy(5_000)
, qui avance le temps de cinq secondes et fait expirer l'autre coroutine.
Il s'agit d'un test du délai avant expiration complet qui se réussira une fois le délai avant expiration implémenté.
Exécutez-le maintenant et observez 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 divulguer des coroutines une fois le test terminé. Si des coroutines sont inachevées, comme notre coroutine de lancement, le test échoue.
Ajouter un délai avant expiration
Ouvrez TitleRepository
et ajoutez un délai avant expiration de cinq secondes pour 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 des tests, tous les tests réussissent.
Dans l'exercice suivant, vous découvrirez comment écrire des fonctions d'ordre supérieur à l'aide de coroutines.
Dans cet exercice, vous allez refactoriser refreshTitle
dans MainViewModel
pour utiliser une fonction générale de chargement des données. Vous allez apprendre à créer des fonctions d'ordre plus élevé 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érales qui affiche toujours l'icône de chargement. Cela peut être utile dans un codebase qui charge des données en réponse à plusieurs événements et qui souhaite s'assurer que l'icône de chargement s'affiche systématiquement.
Chaque ligne, à l'exception de repository.refreshTitle()
, passe en revue le code récurrent pour afficher les erreurs liées à l'icône de chargement et à l'affichage.
// 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
Ajouter 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 refreshTitle()
pour utiliser cette fonction de niveau supérieur.
MainViewModel.kt
fun refreshTitle() {
launchDataLoad {
repository.refreshTitle()
}
}
En abstrait la logique qui consiste à afficher une icône de chargement et à afficher les erreurs, nous avons simplifié notre code nécessaire pour charger les données. L'affichage d'une icône de chargement ou d'une erreur est un processus facile à généraliser pour les chargements de données, tandis que la source et la destination doivent être spécifiées à chaque fois.
Pour créer cette abstraction, launchDataLoad
utilise un argument block
qui est un lambda de suspension. Un lambda de suspension vous permet d'appeler des fonctions de suspension. C'est ainsi que Kotlin met en œuvre les compilateurs 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 de suspension, commencez par le mot clé suspend
. La flèche de fonction et le type renvoyé Unit
remplissent la déclaration.
Il n'est pas souvent nécessaire de déclarer vos propres lambdas de suspension, mais ils peuvent être utiles pour créer des abstractions de ce type encapsulant une logique répétée.
Dans cet exercice, vous allez apprendre à utiliser du code basé sur des coroutines dans 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 à des coroutines. WorkManager est une bibliothèque compatible, 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.
Partie intégrante d'Android Jetpack, WorkManager est un composant d'architecture permettant d'exécuter en arrière-plan un travail qui nécessite une exécution à la fois 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.
Pour cette raison, WorkManager est un choix judicieux pour les tâches qui doivent se terminer.
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 propose différentes implémentations de sa classe ListanableWorker
de base pour différents cas d'utilisation.
La classe de nœud de calcul la plus simple nous permet d'exécuter une opération synchrone par WorkManager. Cependant, jusqu'à présent, pour convertir notre codebase afin d'utiliser des coroutines et des fonctions de suspension, la meilleure façon d'utiliser WorkManager est de passer par la classe CoroutineWorker
, qui permet de définir notre fonction doWork()
en tant que fonction de suspension.
Pour commencer, ouvrez RefreshMainDataWork
. Elle é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 que vous avez terminé le TODO, 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 dans l'exécuteur spécifié dans votre configuration WorkManager, mais utilise le coordinateur dans le membre coroutineContext
(par défaut, Dispatchers.Default
).
Tester notre CoroutineWorker
Aucun codebase ne doit être terminé sans le test.
WorkManager propose deux méthodes pour 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 permettant de tester plus facilement les classes ListenableWorker
et, par conséquent, 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, indiquez WorkManager
à l'usine afin d'injecter le faux réseau.
Le test lui-même utilise TestListenableWorkerBuilder
pour créer le nœud de calcul, que nous pouvons ensuite exécuter en appelant la méthode startWork()
.
WorkManager n'est qu'un exemple d'utilisation des coroutines pour simplifier la conception des API.
Dans cet atelier de programmation, nous avons passé en revue les notions élémentaires que vous devez connaître 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 UI et WorkManager afin de simplifier la programmation asynchrone
- Utilisez 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. - Annulez toutes les coroutines lorsque
ViewModel
est terminé.
Pour tester le code basé sur des coroutines, nous avons abordé les comportements en testant et en appelant directement les fonctions suspend
à partir de tests.
En savoir plus
Consultez l'atelier de programmation "Coroutines avancées avec Kotlin Flow et LiveData" pour en savoir plus sur l'utilisation de coroutines avancées 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 pour les coroutines publiés par JetBrains. Pensez également à améliorer les performances de l'application avec des coroutines Kotlin et à découvrir d'autres modèles d'utilisation des coroutines sur Android.