1. Introduction
Dans l'atelier de programmation Travail en arrière-plan avec WorkManager, vous avez appris à exécuter des tâches en arrière-plan (et non sur le thread principal) à l'aide de WorkManager. Dans cet atelier de programmation, vous allez découvrir les fonctionnalités de WorkManager qui permettent d'exécuter des séquences de tâches uniques, d'annuler des tâches, d'y ajouter des balises et de définir des contraintes de tâche. À la fin de cet atelier de programmation, vous verrez comment écrire des tests automatisés pour vérifier que vos workers fonctionnent correctement et qu'ils renvoient les résultats attendus. Vous apprendrez également à inspecter des workers en file d'attente à l'aide de l'outil Background Task Inspector fourni par Android Studio.
Objectifs de l'atelier
Dans cet atelier de programmation, vous allez créer des séquences de tâches uniques, annuler des tâches, y ajouter des balises et mettre en œuvre des contraintes. Vous découvrirez ensuite comment écrire des tests d'interface utilisateur automatisés pour l'application Blur-O-Matic. Ces tests vérifieront le fonctionnement des trois workers créés dans l'atelier de programmation Travail en arrière-plan avec WorkManager :
BlurWorker
CleanupWorker
SaveImageToFileWorker
Points abordés
- Créer des séquences de tâches uniques
- Annuler une tâche
- Définir des contraintes de tâche
- Écrire des tests automatisés pour vérifier le fonctionnement des workers
- Principes de base de l'inspection des workers en file d'attente avec Background Task Inspector
Ce dont vous avez besoin
- Vous disposez de la dernière version stable d'Android Studio.
- Vous avez mené à bien l'atelier de programmation Travail en arrière-plan avec WorkManager.
- Vous disposez d'un appareil ou d'un émulateur Android.
2. Configuration
Télécharger le code
Cliquez sur le lien ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation :
Ou, si vous préférez, vous pouvez cloner le code depuis GitHub :
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git $ cd basic-android-kotlin-compose-training-workmanager $ git checkout intermediate
Ouvrez le projet dans Android Studio.
3. Créer des séquences de tâches uniques
Maintenant que vous savez comment relier les workers, intéressons-nous à une autre fonctionnalité puissante de WorkManager : les séquences de tâches uniques.
Il peut arriver que vous ne souhaitiez exécuter qu'une seule chaîne de travail à la fois. Par exemple, si vous avez une chaîne de travail qui synchronise vos données locales avec le serveur, vous souhaitez probablement que la première synchronisation des données se termine avant qu'une nouvelle démarre. Pour ce faire, vous devez utiliser beginUniqueWork()
au lieu de beginWith()
, et indiquer un nom unique pour String
. Ce nom qualifie l'intégralité de la chaîne de requêtes de travail. Vous pouvez ainsi y faire référence et les interroger ensemble.
Vous devez également transmettre un objet ExistingWorkPolicy
. Cet objet indique à l'OS Android ce qui se passe si la tâche existe déjà. Les valeurs ExistingWorkPolicy
possibles sont REPLACE
, KEEP
, APPEND
, ou APPEND_OR_REPLACE
.
Dans cette application, vous utiliserez REPLACE
, car si un utilisateur décide de flouter une autre image avant que l'image actuelle soit terminée, l'image actuelle doit être arrêtée avant de flouter la nouvelle.
Vous voulez également vous assurer que si un utilisateur clique sur Start (Commencer) lorsqu'une requête de travail est déjà en file d'attente, l'application remplace la requête précédente par la nouvelle. Il n'est pas logique de continuer à travailler sur la requête précédente, car l'application la remplace de toute façon par la nouvelle.
Dans le fichier data/WorkManagerBluromaticRepository.kt
, dans la méthode applyBlur()
, procédez comme suit :
- Supprimez l'appel de la fonction
beginWith()
et ajoutez un appel à la fonctionbeginUniqueWork()
. - Pour le premier paramètre de la fonction
beginUniqueWork()
, transmettez la constanteIMAGE_MANIPULATION_WORK_NAME
. - Pour le deuxième paramètre,
existingWorkPolicy
, transmettezExistingWorkPolicy.REPLACE
. - Pour le troisième paramètre, créez un objet
OneTimeWorkRequest
pourCleanupWorker
.
data/WorkManagerBluromaticRepository.kt
import androidx.work.ExistingWorkPolicy
import com.example.bluromatic.IMAGE_MANIPULATION_WORK_NAME
...
// REPLACE THIS CODE:
// var continuation = workManager.beginWith(OneTimeWorkRequest.from(CleanupWorker::class.java))
// WITH
var continuation = workManager
.beginUniqueWork(
IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker::class.java)
)
...
Blur-O-Matic ne floute désormais qu'une image à la fois.
4. Ajouter des balises à l'UI et la mettre à jour en fonction de l'état d'une tâche
La modification suivante concerne ce que l'application affiche lorsque la tâche s'exécute. Les informations renvoyées concernant les tâches mises en file d'attente déterminent les modifications à apporter à l'interface utilisateur.
Ce tableau présente trois méthodes différentes que vous pouvez appeler pour obtenir des informations sur une tâche :
Type | Méthode WorkManager | Description |
Obtention de la tâche à partir de l'ID | Cette fonction renvoie un seul élément LiveData<WorkInfo> pour une requête de travail spécifique par son ID. | |
Obtention de la tâche à partir d'un nom de chaîne unique | Cette fonction renvoie LiveData<List<WorkInfo>> pour toutes les tâches d'une chaîne unique de requêtes de travail. | |
Obtention de la tâche à partir d'une balise | Cette fonction renvoie la valeur LiveData<List<WorkInfo>> pour une balise. |
Un objet WorkInfo
contient des détails sur l'état actuel d'un élément WorkRequest
, y compris :
- si l'état de la tâche indique
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
ouSUCCEEDED
; - si l'élément
WorkRequest
est terminé et s'il existe des données de sortie pour la tâche.
Ces méthodes renvoient LiveData. LiveData est un conteneur de données observables tenant compte du cycle de vie. Nous le convertissons en flux d'objets WorkInfo
en appelant .asFlow()
.
Étant donné que ce qui vous intéresse est le moment où l'image finale est enregistrée, vous ajoutez une balise à la requête de travail SaveImageToFileWorker
afin de pouvoir obtenir des informations sur la tâche à partir de la méthode getWorkInfosByTagLiveData()
.
Une autre option consiste à utiliser la méthode getWorkInfosForUniqueWorkLiveData()
, qui renvoie des informations sur les trois requêtes de travail (CleanupWorker
, BlurWorker
et SaveImageToFileWorker
). L'inconvénient est que vous avez ici besoin de code supplémentaire pour trouver spécifiquement les informations SaveImageToFileWorker
nécessaires.
Ajouter des balises à la requête de travail
L'ajout de balises s'effectue dans le fichier data/WorkManagerBluromaticRepository.kt
de la fonction applyBlur()
.
- Lorsque vous créez la requête de travail
SaveImageToFileWorker
, ajoutez des balises à la tâche en appelant la méthodeaddTag()
et en transmettant la constanteString
TAG_OUTPUT
.
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.TAG_OUTPUT
...
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.addTag(TAG_OUTPUT) // <- Add this
.build()
Au lieu d'un ID WorkManager, vous pouvez utiliser une balise pour libeller une tâche. En effet, si un utilisateur floute plusieurs images, tous les objets WorkRequest
d'enregistrement d'image auront la même balise, mais pas le même ID.
Obtenir des informations sur une tâche
Les informations WorkInfo
de la requête de travail SaveImageToFileWorker
dans la logique vous permettent de déterminer les composables à afficher dans l'UI en fonction du BlurUiState
.
ViewModel utilise ces informations à partir de la variable outputWorkInfo
du dépôt.
Maintenant que vous avez ajouté une balise à la requête de travail SaveImageToFileWorker
, vous pouvez suivre les étapes ci-dessous pour récupérer ses informations :
- Dans le fichier
data/WorkManagerBluromaticRepository.kt
, appelez la méthodeworkManager.getWorkInfosByTagLiveData()
pour renseigner la variableoutputWorkInfo
. - Transmettez la constante
TAG_OUTPUT
pour le paramètre de la méthode.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
...
L'appel de la méthode getWorkInfosByTagLiveData()
renvoie LiveData. LiveData est un conteneur de données observables tenant compte du cycle de vie. La fonction .asFlow()
le convertit en flux.
- Associez un appel à la fonction
.asFlow()
pour convertir la méthode en flux. Vous convertissez la méthode afin que l'application puisse fonctionner avec un flux Kotlin au lieu de LiveData.
data/WorkManagerBluromaticRepository.kt
import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
- Associez un appel à la fonction de transformation
.mapNotNull()
pour vous assurer que le flux contient des valeurs. - Pour la règle de transformation, si l'élément n'est pas vide, sélectionnez le premier élément de la collection. Sinon, renvoyez une valeur nulle. La fonction de transformation les supprimera ensuite si elles ont une valeur nulle.
data/WorkManagerBluromaticRepository.kt
import kotlinx.coroutines.flow.mapNotNull
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow().mapNotNull {
if (it.isNotEmpty()) it.first() else null
}
...
- Étant donné que la fonction de transformation
.mapNotNull()
garantit l'existence d'une valeur, vous pouvez supprimer en toute sécurité le signe?
du type Flow, car il n'a plus besoin d'avoir une valeur nulle.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo> =
...
- Supprimez également
?
de l'interfaceBluromaticRepository
.
data/BluromaticRepository.kt
...
interface BluromaticRepository {
// val outputWorkInfo: Flow<WorkInfo?>
val outputWorkInfo: Flow<WorkInfo>
...
Les informations WorkInfo
sont émises en tant que Flow
à partir du dépôt. Le ViewModel
les consomme ensuite.
Mettre à jour BlurUiState
ViewModel
utilise les informations WorkInfo
émises par le dépôt à partir du flux outputWorkInfo
pour définir la valeur de la variable blurUiState
.
Le code de l'UI utilise la valeur de la variable blurUiState
pour déterminer les composables à afficher.
Pour mettre à jour la variable blurUiState
, procédez comme suit :
- Renseignez la variable
blurUiState
avec le fluxoutputWorkInfo
du dépôt.
ui/BlurViewModel.kt
// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)
// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
- Mappez ensuite les valeurs du flux avec les états
BlurUiState
, en fonction de l'état de la tâche.
Lorsque la tâche est terminée, définissez la variable blurUiState
sur BlurUiState.Complete(outputUri = "")
.
Lorsque la tâche est annulée, définissez la variable blurUiState
sur BlurUiState.Default
.
Sinon, définissez la variable blurUiState
sur BlurUiState.Loading
.
ui/BlurViewModel.kt
import androidx.work.WorkInfo
import kotlinx.coroutines.flow.map
// ...
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
.map { info ->
when {
info.state.isFinished -> {
BlurUiState.Complete(outputUri = "")
}
info.state == WorkInfo.State.CANCELLED -> {
BlurUiState.Default
}
else -> BlurUiState.Loading
}
}
// ...
- Étant donné que ce qui vous intéresse est un StateFlow, convertissez le flux en associant un appel à la fonction
.stateIn()
.
L'appel de la fonction .stateIn()
nécessite trois arguments :
- Pour le premier paramètre, transmettez
viewModelScope
, qui est le champ d'application de la coroutine lié au ViewModel. - Pour le deuxième paramètre, transmettez
SharingStarted.WhileSubscribed(5_000)
. Ce paramètre contrôle le début et la fin du partage. - Pour le troisième paramètre, transmettez
BlurUiState.Default
, qui est la valeur initiale du flux d'état.
ui/BlurViewModel.kt
import kotlinx.coroutines.flow.stateIn
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
// ...
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
.map { info ->
when {
info.state.isFinished -> {
BlurUiState.Complete(outputUri = "")
}
info.state == WorkInfo.State.CANCELLED -> {
BlurUiState.Default
}
else -> BlurUiState.Loading
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = BlurUiState.Default
)
// ...
ViewModel
expose les informations d'état de l'UI sous forme de StateFlow
via la variable blurUiState
. Le flux passe d'un Flow
froid à un StateFlow
chaud en appelant la fonction stateIn()
.
Mettre à jour l'UI
Le fichier ui/BluromaticScreen.kt
vous permet d'obtenir l'état de l'UI à partir de la variable blurUiState
de ViewModel
et de mettre à jour l'UI.
Un bloc when
contrôle l'UI de l'application. Ce bloc when
comporte une branche pour chacun des trois états BlurUiState
.
L'UI se met à jour dans le composable BlurActions
au sein de son composable Row
. Procédez comme suit :
- Supprimez le code
Button(onStartClick)
dans le composableRow
et remplacez-le par un blocwhen
en utilisantblurUiState
comme argument.
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
// REMOVE
// Button(
// onClick = onStartClick,
// modifier = Modifier.fillMaxWidth()
// ) {
// Text(stringResource(R.string.start))
// }
// ADD
when (blurUiState) {
}
}
...
Lorsque l'application s'ouvre, elle se trouve à son état par défaut. Dans le code, cet état est représenté par BlurUiState.Default
.
- Dans le bloc
when
, créez une branche pour cet état, comme illustré dans l'exemple de code suivant :
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {}
}
}
...
Pour l'état par défaut, le bouton Start (Commencer) s'affiche.
- Pour le paramètre
onClick
à l'étatBlurUiState.Default
, transmettez la variableonStartClick
, qui est transmise au composable. - Pour le paramètre
stringResourceId
, transmettez l'ID de ressource de chaîneR.string.start
.
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(
onClick = onStartClick,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.start))
}
}
}
...
Lorsque l'application floute activement une image, l'état est BlurUiState.Loading
. Dans ce cas, l'application affiche le bouton Cancel Work (Annuler la tâche), ainsi qu'un indicateur de progression circulaire.
- Pour le paramètre
onClick
du bouton à l'étatBlurUiState.Loading
, transmettez la variableonCancelClick
, qui est transmise au composable. - Pour le paramètre
stringResourceId
du bouton, transmettez l'ID de ressource de chaîneR.string.cancel_work
.
ui/BluromaticScreen.kt
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
is BlurUiState.Loading -> {
FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
}
}
}
...
Le dernier état à configurer est l'état BlurUiState.Complete
, qui intervient après le floutage et l'enregistrement d'une image. Pour le moment, l'application n'affiche que le bouton Start (Commencer).
- Pour son paramètre
onClick
à l'étatBlurUiState.Complete
, transmettez la variableonStartClick
. - Pour son paramètre
stringResourceId
, transmettez l'ID de ressource de chaîneR.string.start
.
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
is BlurUiState.Loading -> {
FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
}
is BlurUiState.Complete -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
}
}
...
Exécuter votre application
- Exécutez votre application et cliquez sur Start (Commencer).
- Consultez la fenêtre Background Task Inspector pour déterminer comment les différents états correspondent à l'UI affichée.
SystemJobService
est le composant responsable de la gestion de l'exécution des workers.
Pendant l'exécution des workers, l'UI affiche le bouton Cancel Work (Annuler la tâche), ainsi qu'un indicateur de progression circulaire.
Une fois que les workers ont terminé, l'UI est mise à jour et affiche alors le bouton Start (Commencer).
5. Afficher le résultat final
Dans cette section, vous allez configurer l'application de sorte qu'elle affiche un bouton See File (Voir le fichier) lorsqu'une image floue est prête à être affichée.
Créer le bouton See File (Voir le fichier)
Le bouton See File (Voir le fichier) ne s'affiche que si l'élément BlurUiState
correspond à Complete
.
- Ouvrez le fichier
ui/BluromaticScreen.kt
et accédez au composableBlurActions
. - Pour ajouter de l'espace entre le bouton Start (Commencer) et le bouton See File (Voir le fichier), insérez un composable
Spacer
dans le blocBlurUiState.Complete
. - Ajoutez un composable
FilledTonalButton
. - Pour le paramètre
onClick
, transmettezonSeeFileClick(blurUiState.outputUri)
. - Ajoutez un composable
Text
pour le paramètre de contenu de l'élémentButton
. - Pour le paramètre
text
de l'élémentText
, utilisez l'ID de ressource de chaîneR.string.see_file
.
ui/BluromaticScreen.kt
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
// ...
is BlurUiState.Complete -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
// Add a spacer and the new button with a "See File" label
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_small)))
FilledTonalButton({ onSeeFileClick(blurUiState.outputUri) })
{ Text(stringResource(R.string.see_file)) }
}
// ...
Mettre à jour l'état blurUiState
L'état BlurUiState
est défini dans ViewModel et dépend de l'état de la requête de travail et éventuellement de la variable bluromaticRepository.outputWorkInfo
.
- Dans le fichier
ui/BlurViewModel.kt
, au sein de la transformationmap()
, créez une variableoutputImageUri
. - Renseignez l'URI de cette nouvelle variable enregistrée à partir de l'objet de données
outputData
.
Vous pouvez récupérer cette chaîne à l'aide de la clé KEY_IMAGE_URI
.
ui/BlurViewModel.kt
import com.example.bluromatic.KEY_IMAGE_URI
// ...
.map { info ->
val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
when {
// ...
- Si le worker a terminé et que la variable est insérée, cela signifie qu'une image floutée peut s'afficher.
Pour vérifier si cette variable est insérée, appelez outputImageUri.isNullOrEmpty()
.
- Mettez à jour la branche
isFinished
pour vérifier que la variable est insérée, puis transmettez la variableoutputImageUri
à l'objet de donnéesBlurUiState.Complete
.
ui/BlurViewModel.kt
// ...
.map { info ->
val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
when {
info.state.isFinished && !outputImageUri.isNullOrEmpty() -> {
BlurUiState.Complete(outputUri = outputImageUri)
}
info.state == WorkInfo.State.CANCELLED -> {
// ...
Créer un code d'événement de clic "See File" (Voir le fichier)
Lorsqu'un utilisateur clique sur le bouton See File (Voir le fichier), son gestionnaire onClick
appelle la fonction qui lui a été attribuée. Cette fonction est transmise en tant qu'argument dans l'appel du composable BlurActions()
.
L'objectif de cette fonction est d'afficher l'image enregistrée à partir de son URI. Elle appelle la fonction d'assistance showBlurredImage()
et transmet l'URI. La fonction d'assistance crée un intent et l'utilise pour lancer une nouvelle activité afin d'afficher l'image enregistrée.
- Ouvrez le fichier
ui/BluromaticScreen.kt
. - Dans la fonction
BluromaticScreenContent()
, dans l'appel de la fonction modulableBlurActions()
, commencez à créer une fonction lambda pour le paramètreonSeeFileClick
, qui utilise un seul paramètre nommécurrentUri
. Cette approche stockera l'URI de l'image enregistrée.
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
onSeeFileClick = { currentUri ->
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
- Dans le corps de la fonction lambda, appelez la fonction d'assistance
showBlurredImage()
. - Pour le premier paramètre, transmettez la variable
context
. - Pour le deuxième paramètre, transmettez la variable
currentUri
.
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
// New lambda code runs when See File button is clicked
onSeeFileClick = { currentUri ->
showBlurredImage(context, currentUri)
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
Exécuter votre application
Exécutez votre application. Le nouveau bouton cliquable See File (Voir le fichier) s'affiche à présent. Il vous permet d'accéder au fichier enregistré :
6. Annuler la tâche
Vous avez précédemment ajouté le bouton Cancel Work (Annuler la tâche). Vous pouvez donc désormais ajouter le code permettant à ce bouton d'être opérationnel. WorkManager vous permet d'annuler une tâche à l'aide de l'ID, de la balise et du nom de chaîne unique.
Dans ce cas, vous devrez annuler la tâche à l'aide de son nom de chaîne unique, car l'objectif est d'annuler toutes les tâches de la chaîne, pas seulement une étape spécifique.
Annuler la tâche via son nom
- Ouvrez le fichier
data/WorkManagerBluromaticRepository.kt
. - Dans la fonction
cancelWork()
, appelez la fonctionworkManager.cancelUniqueWork()
. - Transmettez le nom de chaîne unique
IMAGE_MANIPULATION_WORK_NAME
afin que l'appel n'annule que les tâches planifiées portant ce nom.
data/WorkManagerBluromaticRepository.kt
override fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
Conformément au principe de séparation des tâches, les fonctions modulables ne doivent pas interagir directement avec le dépôt. Elles interagissent avec ViewModel, qui interagit ensuite avec le dépôt.
Cette approche est un principe de conception qu'il est conseillé de suivre, car les modifications apportées au dépôt ne nécessitent pas la modification de vos fonctions modulables, car elles n'interagissent pas directement.
- Ouvrez le fichier
ui/BlurViewModel.kt
. - Créez une fonction appelée
cancelWork()
pour annuler la tâche. - Dans la fonction, au niveau de l'objet
bluromaticRepository
, appelez la méthodecancelWork()
.
ui/BlurViewModel.kt
/**
* Call method from repository to cancel any ongoing WorkRequest
* */
fun cancelWork() {
bluromaticRepository.cancelWork()
}
Configurer un événement de clic "Cancel Work" (Annuler la tâche)
- Ouvrez le fichier
ui/BluromaticScreen.kt
. - Accédez à la fonction modulable
BluromaticScreen()
.
ui/BluromaticScreen.kt
fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) {
val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle()
val layoutDirection = LocalLayoutDirection.current
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.padding(
start = WindowInsets.safeDrawing
.asPaddingValues()
.calculateStartPadding(layoutDirection),
end = WindowInsets.safeDrawing
.asPaddingValues()
.calculateEndPadding(layoutDirection)
)
) {
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = {},
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
Dans l'appel du composable BluromaticScreenContent
, vous voulez que la méthode cancelWork()
du ViewModel s'exécute lorsqu'un utilisateur clique sur le bouton.
- Attribuez la valeur
blurViewModel::cancelWork
au paramètrecancelWork
.
ui/BluromaticScreen.kt
// ...
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = blurViewModel::cancelWork,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
// ...
Exécuter l'application et annuler la tâche
Exécutez votre application. Elle devrait se compiler correctement. Commencez à flouter une image, puis cliquez sur Cancel Work (Annuler la tâche). Toute la chaîne est annulée.
Une fois la tâche annulée, seul le bouton Start (Commencer) s'affiche, car WorkInfo.State
est CANCELLED
. Avec cette modification, la variable blurUiState
est définie sur BlurUiState.Default
, ce qui rétablit l'état initial de l'UI et n'affiche que le bouton Start (Commencer).
Background Task Inspector indique l'état Cancelled (Annulé), ce qui est normal.
7. Contraintes liées aux tâches
WorkManager
prend également en charge les contraintes, ou Constraints
. Une contrainte est une exigence que vous devez respecter avant d'exécuter une requête de travail.
Voici deux exemples de contraintes : requiresDeviceIdle()
et requiresStorageNotLow()
.
- Pour la contrainte
requiresDeviceIdle()
, si la valeurtrue
est transmise, la tâche ne s'exécute que si l'appareil est inactif. - Pour la contrainte
requiresStorageNotLow()
, si la valeurtrue
est transmise, la tâche ne s'exécute que si l'espace de stockage n'est pas faible.
Pour Blur-O-Matic, vous ajouterez la contrainte selon laquelle le niveau de charge de la batterie de l'appareil ne doit pas être faible avant d'exécuter la requête de travail blurWorker
. Autrement dit, votre requête de travail est différée et ne s'exécute que lorsque le niveau de la batterie de l'appareil n'est pas faible.
Créer une contrainte de batterie non faible
Dans le fichier data/WorkManagerBluromaticRepository.kt
, procédez comme suit :
- Accédez à la méthode
applyBlur()
. - Une fois que le code a déclaré la variable
continuation
, créez une variable nomméeconstraints
, qui contient un objetConstraints
pour la contrainte qui est en cours de création. - Créez un compilateur pour un objet "Constraints". Pour cela, appelez la fonction
Constraints.Builder()
et attribuez-la à la nouvelle variable.
data/WorkManagerBluromaticRepository.kt
import androidx.work.Constraints
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
// ...
- Associez la méthode
setRequiresBatteryNotLow()
à l'appel et transmettez-lui la valeurtrue
, afin que laWorkRequest
ne s'exécute que lorsque le niveau de la batterie de l'appareil n'est pas faible.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
// ...
- Pour créer l'objet, associez un appel à la méthode
.build()
.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build()
// ...
- Pour ajouter l'objet de contrainte à la requête de travail
blurBuilder
, associez un appel à la méthode.setConstraints()
et transmettez l'objet.
data/WorkManagerBluromaticRepository.kt
// ...
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))
blurBuilder.setConstraints(constraints) // Add this code
//...
Effectuer un test avec l'émulateur
- Sur un émulateur, réglez le niveau de charge dans la fenêtre Extended Controls (Commandes avancées) sur 15 % ou moins pour simuler un scénario de batterie faible, la connexion au chargeur sur chargeur secteur et l'état de la batterie sur ne charge pas.
- Exécutez l'application, puis cliquez sur Start (Commencer) pour commencer à flouter l'image.
Le niveau de charge de la batterie de l'émulateur est faible. WorkManager
n'exécute donc pas la requête de travail blurWorker
en raison de la contrainte. Elle est mise en file d'attente et différée jusqu'à ce que les conditions de la contrainte soient remplies. Ce report est visible dans l'onglet Background Task Inspector.
- Après avoir confirmé que la requête de travail ne s'est pas exécutée, augmentez progressivement le niveau de charge de la batterie.
La contrainte est respectée lorsque le niveau de charge de la batterie atteint environ 25 % et que la tâche différée est exécutée. Ce résultat s'affiche dans l'onglet Background Task Inspector.
8. Écrire des tests pour les implémentations de workers
Tester WorkManager
Il peut être contre-intuitif d'écrire des tests pour les workers et de réaliser des tests à l'aide de l'API WorkManager. Le travail réalisé par un worker ne dispose pas d'un accès direct à l'UI. Il s'agit strictement d'une logique métier. En règle générale, ce sont les tests unitaires locaux qui permettent de tester la logique métier. Toutefois, vous vous souvenez peut-être de l'atelier de programmation "Travail en arrière-plan avec WorkManager" où WorkManager nécessite un contexte Android pour s'exécuter. Par défaut, le contexte n'est pas disponible dans les tests unitaires locaux. Par conséquent, vous devez tester les tests de workers avec des tests d'UI, même s'il n'y a pas d'éléments d'UI directs à tester.
Configurer des dépendances
Vous devez ajouter trois dépendances Gradle à votre projet. Les deux premières activent JUnit et espresso pour les tests de l'UI. La troisième dépendance fournit l'API de test des tâches.
app/build.gradle.kts
dependencies {
// Espresso
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// Junit
androidTestImplementation("androidx.test.ext:junit:1.1.5")
// Work testing
androidTestImplementation("androidx.work:work-testing:2.8.1")
}
Vous devez utiliser la version stable la plus récente de work-runtime-ktx
dans votre application. Si vous changez de version, veillez à cliquer sur Sync Now (Synchroniser) pour synchroniser votre projet avec les fichiers Gradle mis à jour.
Créer une classe de test
- Créez un répertoire pour vos tests d'interface utilisateur dans le répertoire app > src.
- Créez une nouvelle classe Kotlin dans le répertoire
androidTest/java
appeléWorkerInstrumentationTest
.
Écrire un test CleanupWorker
Suivez les étapes ci-dessous pour écrire un test de validation de l'implémentation de CleanupWorker
. Essayez d'effectuer cette validation vous-même en suivant les instructions. Vous trouverez la solution à la fin de ces étapes.
- Dans
WorkerInstrumentationTest.kt
, créez une variablelateinit
pour contenir une instance deContext
. - Créez une méthode
setUp()
annotée avec@Before
. - Dans la méthode
setUp()
, initialisez la variable de contextelateinit
avec un contexte d'application d'ApplicationProvider
. - Créez une fonction de test appelée
cleanupWorker_doWork_resultSuccess()
. - Dans le test
cleanupWorker_doWork_resultSuccess()
, créez une instance deCleanupWorker
.
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
}
}
Lorsque vous écrivez l'application Blur-O-Matic, vous utilisez OneTimeWorkRequestBuilder
pour créer des workers. Les workers de test nécessitent différents compilateurs de tâches. L'API WorkManager fournit deux compilateurs distincts :
Ces deux compilateurs vous permettent de tester la logique métier du worker. Pour les CoroutineWorkers
tels que CleanupWorker
, BlurWorker
et SaveImageToFileWorker
, utilisez TestListenableWorkerBuilder
pour les tests, car il gère les complexités liées à la définition des threads de la coroutine.
- Les
CoroutineWorker
s'exécutent de manière asynchrone, étant donné l'utilisation de coroutines. Pour exécuter le worker en parallèle, utilisezrunBlocking
. Fournissez un corps lambda vide pour commencer, mais utilisezrunBlocking
pour indiquer directement au worker d'effectuer une action directe,doWork()
, au lieu de le mettre en file d'attente.
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
runBlocking {
}
}
}
- Dans le corps lambda de
runBlocking
, appelezdoWork()
au niveau de l'instance deCleanupWorker
que vous avez créée à l'étape 5 et enregistrez-le en tant que valeur.
Vous vous souvenez peut-être que CleanupWorker
supprime tous les fichiers PNG enregistrés dans la structure de fichiers de l'application Blur-O-Matic. Ce processus implique une entrée/sortie de fichier, ce qui signifie que des exceptions peuvent être générées lors de la tentative de suppression de fichiers. C'est pour cette raison que la tentative de suppression des fichiers est encapsulée dans un bloc try
.
CleanupWorker.kt
...
return@withContext try {
val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
if (outputDirectory.exists()) {
val entries = outputDirectory.listFiles()
if (entries != null) {
for (entry in entries) {
val name = entry.name
if (name.isNotEmpty() && name.endsWith(".png")) {
val deleted = entry.delete()
Log.i(TAG, "Deleted $name - $deleted")
}
}
}
}
Result.success()
} catch (exception: Exception) {
Log.e(
TAG,
applicationContext.resources.getString(R.string.error_cleaning_file),
exception
)
Result.failure()
}
Notez qu'à la fin du bloc try
, Result.success()
est renvoyé. Si le code se termine par Result.success()
, aucune erreur d'accès au répertoire de fichiers n'est enregistrée.
Le moment est venu de créer une assertion qui indique que le worker a abouti.
- Confirmez que le résultat du worker est
ListenableWorker.Result.success()
.
Examinez le code de solution suivant :
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
runBlocking {
val result = worker.doWork()
assertTrue(result is ListenableWorker.Result.Success)
}
}
}
Écrire un test BlurWorker
Suivez ces étapes pour écrire un test de validation de l'implémentation de BlurWorker
. Essayez d'effectuer cette validation vous-même en suivant les instructions. Vous trouverez la solution à la fin de ces étapes.
- Dans
WorkerInstrumentationTest.kt
, créez une fonction de test nomméeblurWorker_doWork_resultSuccessReturnsUri()
.
BlurWorker
a besoin d'une image à traiter. Par conséquent, la création d'une instance de BlurWorker
nécessite des données d'entrée qui incluent cette image.
- En dehors de la fonction de test, créez une entrée d'URI fictive. L'URI fictive est une paire qui contient une clé et une valeur. Utilisez l'exemple de code suivant pour la paire clé-valeur :
KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
- Créez un élément
BlurWorker
dans la fonctionblurWorker_doWork_resultSuccessReturnsUri()
et assurez-vous de transmettre l'URI d'entrée fictive que vous avez créée en tant que données de la tâche via la méthodesetInputData()
.
Comme pour le test CleanupWorker
, vous devez appeler l'implémentation du worker dans runBlocking
.
- Créez un bloc
runBlocking
. - Appelez
doWork()
dans le blocrunBlocking
.
Contrairement à CleanupWorker
, BlurWorker
contient des données de sortie prêtes à être testées.
- Pour accéder aux données de sortie, extrayez l'URI du résultat de
doWork()
.
WorkerInstrumentationTest.kt
@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
val worker = TestListenableWorkerBuilder<BlurWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
}
}
- Confirmez que le worker a bien été créé. Pour obtenir un exemple, examinez le code suivant à partir de
BlurWorker
:
BlurWorker.kt
val resourceUri = inputData.getString(KEY_IMAGE_URI)
val blurLevel = inputData.getInt(BLUR_LEVEL, 1)
...
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri))
)
val output = blurBitmap(picture, blurLevel)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(applicationContext, output)
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
...
BlurWorker
extrait l'URI et le niveau de flou des données d'entrée et crée un fichier temporaire. Si l'opération réussit, il renvoie une paire clé-valeur contenant l'URI. Pour vérifier que le contenu de la sortie est correct, confirmez que les données de sortie contiennent la clé KEY_IMAGE_URI
.
- Confirmez que les données de sortie contiennent un URI commençant par la chaîne
"file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-"
.
- Comparez votre test au code de solution suivant :
WorkerInstrumentationTest.kt
@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
val worker = TestListenableWorkerBuilder<BlurWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
assertTrue(result is ListenableWorker.Result.Success)
assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
assertTrue(
resultUri?.startsWith("file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-")
?: false
)
}
}
Écrire un test SaveImageToFileWorker
Comme son nom l'indique, SaveImageToFileWorker
écrit un fichier sur le disque. Rappelez-vous que dans WorkManagerBluromaticRepository
, vous ajoutez SaveImageToFileWorker
à WorkManager en continuité de BlurWorker
. Par conséquent, les données d'entrée sont les mêmes. Il extrait l'URI des données d'entrée, crée un bitmap, puis écrit ce bitmap sur le disque en tant que fichier. Si l'opération aboutit, le résultat est une URL d'image. Le test pour SaveImageToFileWorker
est très semblable à celui du test BlurWorker
. La seule différence réside dans les données de sortie.
Essayez d'écrire vous-même un test pour SaveImageToFileWorker
. Lorsque vous avez terminé, vous pouvez consulter la solution ci-dessous. Rappelez-vous l'approche que vous avez adoptée pour le test BlurWorker
:
- Créez le worker en transmettant les données d'entrée.
- Créez un bloc
runBlocking
. - Appelez
doWork()
au niveau du worker. - Vérifiez que le résultat a abouti.
- Vérifiez que la clé et la valeur sont correctes dans la sortie.
Voici la solution :
@Test
fun saveImageToFileWorker_doWork_resultSuccessReturnsUrl() {
val worker = TestListenableWorkerBuilder<SaveImageToFileWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
assertTrue(result is ListenableWorker.Result.Success)
assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
assertTrue(
resultUri?.startsWith("content://media/external/images/media/")
?: false
)
}
}
9. Déboguer WorkManager avec Background Task Inspector
Inspecter les workers
Les tests automatisés sont un excellent moyen de vérifier le fonctionnement des workers. Toutefois, ils ne sont pas très utiles lorsque vous essayez de déboguer un worker : Heureusement, Android Studio dispose d'un outil qui vous permet de visualiser, de surveiller et de déboguer les workers en temps réel. Background Task Inspector est compatible avec les émulateurs et les appareils exécutant une API de niveau 26 ou supérieur.
Dans cette section, vous allez découvrir certaines des fonctionnalités fournies par Background Task Inspector pour inspecter les workers dans Blur-O-Matic.
- Lancez l'application Blur-O-Matic sur un appareil ou dans un émulateur.
- Accédez à View > Tool Windows > App Inspection (Vue > Fenêtres d'outils > Inspection des applications).
- Sélectionnez l'onglet Background Task Inspector.
- Si nécessaire, sélectionnez l'appareil et le processus en cours d'exécution dans le menu déroulant.
Dans les exemples d'images, le processus correspond à com.example.bluromatic
. Il se peut que le processus soit sélectionné automatiquement. En cas d'erreur, vous pouvez le modifier.
- Cliquez sur le menu déroulant Workers. Aucun worker n'est en cours d'exécution, ce qui est logique, car aucune tentative visant à flouter une image n'a été effectuée.
- Dans l'application, sélectionnez More blurred (Plus flouté), puis cliquez sur Start (Commencer). Vous verrez immédiatement du contenu s'afficher dans la liste déroulante Workers.
Un message semblable au suivant s'affiche maintenant dans le menu déroulant Workers.
Le tableau "Workers" affiche le nom du worker, le service (SystemJobService
dans ce cas), l'état de chaque worker et un horodatage. Dans la capture d'écran de l'étape précédente, notez que BlurWorker
et CleanupWorker
ont bien terminé leur tâche.
Vous pouvez également annuler des tâches à l'aide de l'inspecteur.
- Sélectionnez un worker mis en file d'attente, puis cliquez sur Cancel Selected Worker (Annuler le worker sélectionné) dans la barre d'outils.
Inspecter les détails de la tâche
- Cliquez sur un worker dans le tableau Workers.
La fenêtre Task Details (Détails de la tâche) s'affiche.
- Vérifiez les informations affichées dans la section Task Details (Détails de la tâche).
Les détails présentent les catégories suivantes :
- Description : cette section liste le nom de la classe du worker avec le package complet, ainsi que la balise attribuée et l'UUID de ce worker.
- Execution (Exécution) : cette section affiche les contraintes du worker (le cas échéant), la fréquence d'exécution, l'état, ainsi que la classe ayant créé et mis en file d'attente le worker. Rappelez-vous que BlurWorker a une contrainte qui l'empêche de s'exécuter lorsque le niveau de la batterie est faible. Lorsque vous inspectez un worker soumis à des contraintes, elles figurent dans cette section.
- WorkContinuation (Continuité de la tâche) : cette section indique où se trouve ce worker dans la chaîne de travail. Pour vérifier les détails d'un autre worker de la chaîne de travail, cliquez sur son UUID.
- Results (Résultats) : cette section affiche l'heure de début, le nombre de nouvelles tentatives et les données de sortie du worker sélectionné.
Vue graphique
Rappelez-vous que les workers de Blur-O-Matic sont enchaînés. Background Task Inspector offre une vue graphique qui représente visuellement les dépendances des workers.
Dans l'angle de la fenêtre Background Task Inspector, se trouvent deux boutons bascules : Show Graph View (Afficher la vue graphique) et Show List View (Afficher la vue sous forme de liste).
- Cliquez sur Show Graph View (Afficher la vue graphique).
La vue graphique indique précisément la dépendance des workers qui a été implémentée dans l'application Blur-O-Matic.
- Cliquez sur Show List View (Afficher la vue sous forme de liste) pour quitter le graphique.
Autres fonctionnalités
L'application Blur-O-Matic n'implémente les workers que pour effectuer les tâches en arrière-plan. Toutefois, vous trouverez des informations supplémentaires sur les outils permettant d'inspecter d'autres types de tâches en arrière-plan dans la documentation de l'outil Background Task Inspector.
10. Télécharger le code de solution
Pour télécharger le code de cet atelier de programmation, utilisez les commandes suivantes :
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git $ cd basic-android-kotlin-compose-training-workmanager $ git checkout main
Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.
Si vous le souhaitez, vous pouvez consulter le code de solution de cet atelier de programmation sur GitHub.
11. Félicitations !
Félicitations ! Vous avez découvert d'autres fonctionnalités de WorkManager, vous avez écrit des tests automatisés pour les workers Blur-O-Matic et vous avez utilisé Background Task Inspector pour les examiner. Dans cet atelier de programmation, vous avez appris ce qui suit :
- Attribuer un nom à des chaînes
WorkRequest
uniques - Ajouter des balises aux
WorkRequest
s - Mettre à jour l'UI en fonction des informations
WorkInfo
- Annuler une
WorkRequest
- Ajouter des contraintes à une
WorkRequest
- API de test WorkManager
- Stratégies à adopter pour tester les implémentations de workers
- Tester les
CoroutineWorker
- Inspecter manuellement les workers et vérifier leur fonctionnement