WorkManager et tests : niveau avancé

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 :

  1. Supprimez l'appel de la fonction beginWith() et ajoutez un appel à la fonction beginUniqueWork().
  2. Pour le premier paramètre de la fonction beginUniqueWork(), transmettez la constante IMAGE_MANIPULATION_WORK_NAME.
  3. Pour le deuxième paramètre, existingWorkPolicy, transmettez ExistingWorkPolicy.REPLACE.
  4. Pour le troisième paramètre, créez un objet OneTimeWorkRequest pour CleanupWorker.

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

getWorkInfoByIdLiveData()

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

getWorkInfosForUniqueWorkLiveData()

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

getWorkInfosByTagLiveData()

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 :

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().

  1. Lorsque vous créez la requête de travail SaveImageToFileWorker, ajoutez des balises à la tâche en appelant la méthode addTag() et en transmettant la constante String 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 :

  1. Dans le fichier data/WorkManagerBluromaticRepository.kt, appelez la méthode workManager.getWorkInfosByTagLiveData() pour renseigner la variable outputWorkInfo.
  2. 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.

  1. 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()
...
  1. Associez un appel à la fonction de transformation .mapNotNull() pour vous assurer que le flux contient des valeurs.
  2. 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
        }
...
  1. É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> =
...
  1. Supprimez également ? de l'interface BluromaticRepository.

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 :

  1. Renseignez la variable blurUiState avec le flux outputWorkInfo du dépôt.

ui/BlurViewModel.kt

// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)

// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
  1. 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
            }
        }

// ...
  1. É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 :

  1. Pour le premier paramètre, transmettez viewModelScope, qui est le champ d'application de la coroutine lié au ViewModel.
  2. Pour le deuxième paramètre, transmettez SharingStarted.WhileSubscribed(5_000). Ce paramètre contrôle le début et la fin du partage.
  3. 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 :

  1. Supprimez le code Button(onStartClick) dans le composable Row et remplacez-le par un bloc when en utilisant blurUiState 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.

  1. 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.

  1. Pour le paramètre onClick à l'état BlurUiState.Default, transmettez la variable onStartClick, qui est transmise au composable.
  2. Pour le paramètre stringResourceId, transmettez l'ID de ressource de chaîne R.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.

  1. Pour le paramètre onClick du bouton à l'état BlurUiState.Loading, transmettez la variable onCancelClick, qui est transmise au composable.
  2. Pour le paramètre stringResourceId du bouton, transmettez l'ID de ressource de chaîne R.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).

  1. Pour son paramètre onClick à l'état BlurUiState.Complete, transmettez la variable onStartClick.
  2. Pour son paramètre stringResourceId, transmettez l'ID de ressource de chaîne R.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

  1. Exécutez votre application et cliquez sur Start (Commencer).
  2. 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.

3395cc370b580b32.png

c5622f923670cf67.png

Une fois que les workers ont terminé, l'UI est mise à jour et affiche alors le bouton Start (Commencer).

97252f864ea042aa.png

81ba9962a8649e70.png

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.

  1. Ouvrez le fichier ui/BluromaticScreen.kt et accédez au composable BlurActions.
  2. 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 bloc BlurUiState.Complete.
  3. Ajoutez un composable FilledTonalButton.
  4. Pour le paramètre onClick, transmettez onSeeFileClick(blurUiState.outputUri).
  5. Ajoutez un composable Text pour le paramètre de contenu de l'élément Button.
  6. Pour le paramètre text de l'élément Text, utilisez l'ID de ressource de chaîne R.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.

  1. Dans le fichier ui/BlurViewModel.kt, au sein de la transformation map(), créez une variable outputImageUri.
  2. 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 {
// ...
  1. 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().

  1. Mettez à jour la branche isFinished pour vérifier que la variable est insérée, puis transmettez la variable outputImageUri à l'objet de données BlurUiState.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.

  1. Ouvrez le fichier ui/BluromaticScreen.kt.
  2. Dans la fonction BluromaticScreenContent(), dans l'appel de la fonction modulable BlurActions(), commencez à créer une fonction lambda pour le paramètre onSeeFileClick, 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()
)
// ...
  1. Dans le corps de la fonction lambda, appelez la fonction d'assistance showBlurredImage().
  2. Pour le premier paramètre, transmettez la variable context.
  3. 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é :

9d76d5d7f231c6b6.png

926e532cc24a0d4f.png

6. Annuler la tâche

5cec830cc8ef647e.png

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

  1. Ouvrez le fichier data/WorkManagerBluromaticRepository.kt.
  2. Dans la fonction cancelWork(), appelez la fonction workManager.cancelUniqueWork().
  3. 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.

  1. Ouvrez le fichier ui/BlurViewModel.kt.
  2. Créez une fonction appelée cancelWork() pour annuler la tâche.
  3. Dans la fonction, au niveau de l'objet bluromaticRepository, appelez la méthode cancelWork().

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)

  1. Ouvrez le fichier ui/BluromaticScreen.kt.
  2. 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.

  1. Attribuez la valeur blurViewModel::cancelWork au paramètre cancelWork.

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.

81ba9962a8649e70.png

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.

7656dd320866172e.png

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 valeur true est transmise, la tâche ne s'exécute que si l'appareil est inactif.
  • Pour la contrainte requiresStorageNotLow(), si la valeur true 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 :

  1. Accédez à la méthode applyBlur().
  2. Une fois que le code a déclaré la variable continuation, créez une variable nommée constraints, qui contient un objet Constraints pour la contrainte qui est en cours de création.
  3. 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()
// ...
  1. Associez la méthode setRequiresBatteryNotLow() à l'appel et transmettez-lui la valeur true, afin que la WorkRequest 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)
// ...
  1. 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()
// ...
  1. 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

  1. 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.

9b0084cb6e1a8672.png

  1. 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.

7518cf0353d04f12.png

  1. 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.

ab189db49e7b8997.png

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

  1. Créez un répertoire pour vos tests d'interface utilisateur dans le répertoire app > src. a7768e9b6ea994d3.png

20cc54de1756c884.png

  1. 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.

  1. Dans WorkerInstrumentationTest.kt, créez une variable lateinit pour contenir une instance de Context.
  2. Créez une méthode setUp() annotée avec @Before.
  3. Dans la méthode setUp(), initialisez la variable de contexte lateinit avec un contexte d'application d'ApplicationProvider.
  4. Créez une fonction de test appelée cleanupWorker_doWork_resultSuccess().
  5. Dans le test cleanupWorker_doWork_resultSuccess(), créez une instance de CleanupWorker.

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.

  1. Les CoroutineWorker s'exécutent de manière asynchrone, étant donné l'utilisation de coroutines. Pour exécuter le worker en parallèle, utilisez runBlocking. Fournissez un corps lambda vide pour commencer, mais utilisez runBlocking 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 {
       }
   }
}
  1. Dans le corps lambda de runBlocking, appelez doWork() au niveau de l'instance de CleanupWorker 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.

  1. 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.

  1. Dans WorkerInstrumentationTest.kt, créez une fonction de test nommée blurWorker_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.

  1. 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"
  1. Créez un élément BlurWorker dans la fonction blurWorker_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éthode setInputData().

Comme pour le test CleanupWorker, vous devez appeler l'implémentation du worker dans runBlocking.

  1. Créez un bloc runBlocking.
  2. Appelez doWork() dans le bloc runBlocking.

Contrairement à CleanupWorker, BlurWorker contient des données de sortie prêtes à être testées.

  1. 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)
    }
}
  1. 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.

  1. 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-".
  1. 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 :

  1. Créez le worker en transmettant les données d'entrée.
  2. Créez un bloc runBlocking.
  3. Appelez doWork() au niveau du worker.
  4. Vérifiez que le résultat a abouti.
  5. 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.

  1. Lancez l'application Blur-O-Matic sur un appareil ou dans un émulateur.
  2. Accédez à View > Tool Windows > App Inspection (Vue > Fenêtres d'outils > Inspection des applications).

798f10dfd8d74bb1.png

  1. Sélectionnez l'onglet Background Task Inspector.

d601998f3754e793.png

  1. 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.

6428a2ab43fc42d1.png

  1. 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.

cf8c466b3fd7fed1.png

  1. 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.

569a8e0c1c6993ce.png

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.

  1. Sélectionnez un worker mis en file d'attente, puis cliquez sur Cancel Selected Worker (Annuler le worker sélectionné) 7108c2a82f64b348.png dans la barre d'outils.

Inspecter les détails de la tâche

  1. Cliquez sur un worker dans le tableau Workers. 97eac5ad23c41127.png

La fenêtre Task Details (Détails de la tâche) s'affiche.

9d4e17f7d4afa6bd.png

  1. Vérifiez les informations affichées dans la section Task Details (Détails de la tâche). 59fa1bf4ad8f4d8d.png

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).

4cd96a8b2773f466.png

  1. Cliquez sur Show Graph View (Afficher la vue graphique). 6f871bb00ad8b11a.png

ece206da18cfd1c9.png

La vue graphique indique précisément la dépendance des workers qui a été implémentée dans l'application Blur-O-Matic.

  1. Cliquez sur Show List View (Afficher la vue sous forme de liste) 669084937ea340f5.png 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 WorkRequests
  • 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