Travail en arrière-plan avec WorkManager

1. Avant de commencer

Cet atelier de programmation porte sur WorkManager, une bibliothèque rétrocompatible, flexible et simple pour exécuter en arrière-plan des tâches différables. WorkManager est le planificateur de tâches recommandé sur Android pour les tâches différables, dont l'exécution est garantie.

Conditions préalables

Points abordés

Objectifs de l'atelier

  • Modifier une application de démarrage pour utiliser WorkManager
  • Implémenter une requête de travail pour flouter une image
  • Implémenter un groupe de tâches en série en enchaînant des tâches
  • Transmettre les données dans les tâches programmées et à partir de celles-ci

Ce dont vous avez besoin

  • La dernière version stable d'Android Studio
  • Une connexion Internet

2. Présentation de l'application

Les smartphones actuels sont presque trop bons pour prendre des photos. Le temps où un photographe pouvait prendre une photo intentionnellement floue d'un sujet mystérieux est révolu.

Dans cet atelier de programmation, vous allez utiliser Blur-O-Matic, une application qui permet de flouter des photos et d'enregistrer les résultats dans un fichier. S'agit-il du monstre du Loch Ness ou d'un jouet de bain ? Grâce à Blur-O-Matic, personne ne le saura jamais.

L'écran contient des cases d'option pour sélectionner le niveau de flou de l'image. Cliquez sur le bouton Start (Démarrer) pour flouter et enregistrer l'image.

Pour le moment, l'application n'effectue aucun floutage et n'enregistre pas l'image finale.

Cet atelier de programmation se concentre sur l'ajout de WorkManager dans l'application, la création de workers pour nettoyer les fichiers temporaires générés lors du floutage d'une image, le floutage d'une photo et l'enregistrement d'une copie finale de la photo que vous pouvez afficher lorsque vous cliquez sur le bouton See File (Voir le fichier). Vous apprendrez également à surveiller l'état du travail en arrière-plan et à mettre à jour l'UI de l'application en conséquence.

3. Explorer l'application de démarrage Blur-O-Matic

Télécharger le code de démarrage

Pour commencer, téléchargez le code de démarrage :

Vous pouvez également cloner le dépôt GitHub du code :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout starter

Vous pouvez parcourir le code de l'application Blur-o-matic dans ce dépôt GitHub.

Exécuter le code de démarrage

Pour vous familiariser avec le code de démarrage, procédez comme suit :

  1. Dans Android Studio, ouvrez le projet contenant le code de démarrage.
  2. Exécutez l'application sur un appareil Android ou sur un émulateur.

2bdb6fdc2567e96.png

L'écran contient des cases d'option permettant de sélectionner le niveau de flou de l'image. Lorsque vous cliquez sur le bouton Start (Démarrer), l'application floute et enregistre l'image.

Pour le moment, l'application n'applique aucun niveau de flou lorsque vous cliquez sur le bouton Start (Démarrer).

Tutoriel du code de démarrage

Dans cette tâche, vous allez vous familiariser avec la structure du projet. Les listes suivantes présentent les fichiers et dossiers importants du projet.

  • WorkerUtils : méthodes pratiques que vous utiliserez par la suite pour afficher les Notifications et code permettant d'enregistrer un bitmap dans un fichier.
  • BlurViewModel : ce modèle de vue stocke l'état de l'application et interagit avec le dépôt.
  • WorkManagerBluromaticRepository : classe dans laquelle vous démarrez le travail en arrière-plan avec WorkManager.
  • Constants : classe statique comprenant quelques constantes que vous utiliserez au cours de l'atelier de programmation.
  • BluromaticScreen : contient des fonctions modulables pour l'UI et interagit avec le BlurViewModel. Les fonctions modulables affichent l'image et incluent des cases d'option permettant de sélectionner le niveau de flou souhaité.

4. Qu'est-ce que WorkManager ?

Partie intégrante d'Android Jetpack, WorkManager est un composant d'architecture permettant d'exécuter en arrière-plan un travail qui nécessite une exécution à la fois opportuniste et garantie. Une exécution opportuniste signifie que WorkManager exécute le travail en arrière-plan dès que possible. Une exécution garantie signifie que WorkManager gère la logique permettant de démarrer le travail dans diverses situations, même si vous quittez l'application.

WorkManager est une bibliothèque incroyablement flexible, qui présente de nombreux avantages. En voici quelques-uns :

  • Prise en charge des tâches asynchrones ponctuelles et périodiques
  • Prise en charge des contraintes telles que l'état du réseau, l'espace de stockage et l'état de charge
  • Enchaînement de requêtes de travail complexes, tel que l'exécution de tâches en parallèle
  • Résultat d'une requête de travail utilisé comme entrée pour la requête suivante
  • Rétrocompatibilité au niveau de l'API avec le niveau d'API 14 (voir la remarque)
  • Fonctionnement avec ou sans les services Google Play
  • Respect des bonnes pratiques concernant l'état du système
  • Possibilité d'afficher facilement l'état des requêtes de travail dans l'UI de l'application

5. Quand utiliser WorkManager

La bibliothèque WorkManager est un excellent choix pour les tâches qu'il est nécessaire de terminer même si l'utilisateur quitte l'application. Une fois qu'elles sont mises en file d'attente, leur exécution se poursuit, et ce que l'application continue à être exécutée ou non. Les tâches s'exécutent même si l'application est fermée ou si l'utilisateur revient à l'écran d'accueil.

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

  • Requêtes d'actualités périodiques
  • Application de filtres à une image et enregistrement de l'image
  • Synchronisation périodique des données locales avec le réseau

WorkManager est une option parmi d'autres permettant d'exécuter une tâche à partir du thread principal, mais elle ne permet pas pour autant d'exécuter tous les types de tâches imaginables. Les coroutines sont une autre option dont nous avons parlé précédemment lors des ateliers de programmation.

Pour savoir quand utiliser WorkManager, consultez le guide sur les tâches en arrière-plan.

6. Ajouter WorkManager à votre application

WorkManager nécessite la dépendance Gradle suivante. Elle est déjà incluse dans le fichier de compilation :

app/build.gradle.kts

dependencies {
    // WorkManager dependency
    implementation("androidx.work:work-runtime-ktx: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.

7. Principes de base de WorkManager

Voici quelques-unes des classes de WorkManager que vous devez connaître :

  • Worker/CoroutineWorker : classe Worker qui exécute la tâche de manière synchrone sur un thread d'arrière-plan. Comme il est question d'une tâche asynchrone, nous pouvons utiliser CoroutineWorker, qui est compatible avec les coroutines Kotlin. Dans cette application, vous allez étendre la classe CoroutineWorker et remplacer la méthode doWork(). Cette méthode vous permet d'insérer le code correspondant au travail à effectuer en arrière-plan.
  • WorkRequest : cette classe représente une requête d'exécution d'une tâche. Un objet WorkRequest vous permet de définir si le worker doit être exécuté une ou plusieurs fois. Des contraintes peuvent également être placées au niveau de l'objet WorkRequest. Dans ce cas, certaines conditions doivent être remplies avant l'exécution de la tâche (par exemple, l'appareil doit être en charge avant de pouvoir commencer la tâche demandée). Vous transmettez CoroutineWorker dans le cadre de la création de l'objet WorkRequest.
  • WorkManager : cette classe planifie votre WorkRequest et l'exécute. Elle planifie un objet WorkRequest de manière à répartir la charge pesant sur les ressources système, tout en respectant les contraintes que vous spécifiez.

Dans votre cas, vous définissez une nouvelle classe BlurWorker, qui contient le code permettant de flouter une image. Lorsque vous cliquez sur le bouton Start (Démarrer), WorkManager crée un objet WorkRequest, puis le place en file d'attente.

8. Créer la classe BlurWorker

Au cours de cette étape, vous allez utiliser une image du dossier res/drawable appelée android_cupcake.png et y exécuter quelques fonctions en arrière-plan. Ces fonctions permettent de flouter l'image.

  1. Effectuez un clic droit sur le package com.example.bluromatic.workers dans le volet de votre projet Android, puis sélectionnez New -> Kotlin Class/File (Nouveau -> Classe/Fichier Kotlin).
  2. Nommez la nouvelle classe Kotlin BlurWorker. Étendez-la à partir de CoroutineWorker avec les paramètres de constructeur requis.

workers/BlurWorker.kt.

import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import android.content.Context

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
}

La classe BlurWorker étend la classe CoroutineWorker au lieu de la classe Worker plus générale. L'implémentation doWork() de la classe CoroutineWorker est une fonction de suspension, qui lui permet d'exécuter du code asynchrone qu'un Worker ne parvient pas à gérer. Comme indiqué dans le guide Exécuter des threads dans WorkManager, "CoroutineWorker est l'implémentation recommandée pour les utilisateurs Kotlin".

À ce stade, Android Studio trace une ligne rouge ondulée sous class BlurWorker pour indiquer une erreur.

9e96aa94f82c6990.png

Si vous placez le curseur au-dessus du texte class BlurWorker, l'IDE affiche un pop-up contenant des informations supplémentaires sur l'erreur.

cdc4bbefa7a9912b.png

Le message d'erreur indique que vous n'avez pas remplacé la méthode doWork() de manière appropriée.

Dans la méthode doWork(), écrivez le code permettant de flouter l'image de cupcake.

Pour corriger l'erreur et implémenter la méthode doWork(), procédez comme suit :

  1. Placez le curseur dans le code de la classe en cliquant sur le texte "BlurWorker".
  2. Dans le menu Android Studio, sélectionnez Code > Override Methods (Code > Ignorer les méthodes).
  3. Dans le pop-up Override Members (Remplacer les membres), sélectionnez doWork().
  4. Cliquez sur OK.

8f495f0861ed19ff.png

  1. Juste avant la déclaration de classe, créez une variable nommée TAG et attribuez-lui la valeur BlurWorker. Notez que cette variable n'est pas spécifiquement liée à la méthode doWork(), mais vous l'utiliserez ultérieurement dans les appels à Log().

workers/BlurWorker.kt

private const val TAG = "BlurWorker"

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
... 
  1. Pour mieux voir à quel moment les tâches s'exécutent, vous devez utiliser la fonction makeStatusNotification() de WorkerUtil. Cette fonction vous permet d'afficher facilement une bannière de notification en haut de l'écran.

Dans la méthode doWork(), utilisez la fonction makeStatusNotification() pour afficher une notification d'état et avertir l'utilisateur que le worker de floutage a démarré et qu'il est en train de flouter l'image.

workers/BlurWorker.kt

import com.example.bluromatic.R
...
override suspend fun doWork(): Result {

    makeStatusNotification(
        applicationContext.resources.getString(R.string.blurring_image),
        applicationContext
    )
...
  1. Ajoutez un bloc de code return try...catch, qui correspond à l'opération de floutage des images.

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
        } catch (throwable: Throwable) {
        }
...
  1. Dans le bloc try, ajoutez un appel à Result.success().
  2. Dans le bloc catch, ajoutez un appel à Result.failure().

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...
  1. Dans le bloc try, créez une variable nommée picture et renseignez-la avec le bitmap renvoyé par l'appel de la méthode BitmapFactory.decodeResource(), ainsi que par la transmission du package de ressources de l'application et l'ID de ressource de l'image de cupcake.

workers/BlurWorker.kt

...
        return try {
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            Result.success()
...
  1. Floutez le bitmap en appelant la fonction blurBitmap() et en transmettant la variable picture et la valeur 1 (un) pour le paramètre blurLevel.
  2. Enregistrez le résultat dans une nouvelle variable nommée output.

workers/BlurWorker.kt

...
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            val output = blurBitmap(picture, 1)

            Result.success()
...
  1. Créez une variable outputUri et renseignez-la avec un appel à la fonction writeBitmapToFile().
  2. Dans l'appel à writeBitmapToFile(), transmettez le contexte de l'application et la variable output en tant qu'arguments.

workers/BlurWorker.kt.

...
            val output = blurBitmap(picture, 1)

            // Write bitmap to a temp file
            val outputUri = writeBitmapToFile(applicationContext, output)

            Result.success()
...
  1. Ajoutez le code permettant d'afficher un message de notification contenant la variable outputUri.

workers/BlurWorker.kt.

...
            val outputUri = writeBitmapToFile(applicationContext, output)

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...
  1. Dans le bloc catch, journalisez un message d'erreur pour indiquer qu'une erreur s'est produite lors du floutage de l'image. L'appel de Log.e() transmet la variable TAG définie précédemment, un message approprié et l'exception générée.

workers/BlurWorker.kt

...
        } catch (throwable: Throwable) {
            Log.e(
                TAG,
                applicationContext.resources.getString(R.string.error_applying_blur),
                throwable
            )
            Result.failure()
        }
...

Par défaut, un CoroutineWorker, est exécuté en tant que Dispatchers.Default, mais vous pouvez le modifier en appelant withContext() et en transmettant le dispatcher souhaité.

  1. Créez un bloc withContext().
  2. Dans l'appel de withContext(), transmettez Dispatchers.IO afin que la fonction lambda s'exécute dans un pool de threads spécial pour pouvoir bloquer les opérations d'E/S.
  3. Déplacez le code return try...catch précédemment écrit vers ce bloc.
...
        return withContext(Dispatchers.IO) {

            return try {
                // ...
            } catch (throwable: Throwable) {
                // ...
            }
        }
...

Android Studio affiche l'erreur suivante, car vous ne pouvez pas appeler return à partir d'une fonction lambda.

2d81a484b1edfd1d.png

Pour corriger cette erreur, ajoutez un libellé comme dans le pop-up.

...
            //return try {
            return@withContext try {
...

Comme ce worker s'exécute très rapidement, il est recommandé d'ajouter un délai dans le code pour émuler une tâche plus lente.

  1. Dans le lambda withContext(), ajoutez un appel à la fonction utilitaire delay() et transmettez la constante DELAY_TIME_MILLIS. Cet appel est exclusivement destiné à l'atelier de programmation afin d'assurer un délai entre les messages de notification.
import com.example.bluromatic.DELAY_TIME_MILLIS
import kotlinx.coroutines.delay

...
        return withContext(Dispatchers.IO) {

            // This is an utility function added to emulate slower work.
            delay(DELAY_TIME_MILLIS)

                val picture = BitmapFactory.decodeResource(
...

9. Mettre à jour WorkManagerBluromaticRepository

Le dépôt gère toutes les interactions avec WorkManager. Cette structure respecte le principe de séparation des tâches, qui est un schéma d'architecture Android recommandé.

  • Dans le fichier data/WorkManagerBluromaticRepository.kt, dans la classe WorkManagerBluromaticRepository, créez une variable privée nommée workManager et stockez-y une instance WorkManager en appelant WorkManager.getInstance(context).

data/WorkManagerBluromaticRepository.kt

import androidx.work.WorkManager
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    // New code
    private val workManager = WorkManager.getInstance(context)
...

Créer la requête de travail et la placer en file d'attente dans WorkManager

L'heure est venue de créer une requête de travail, ou WorkRequest, et de demander à WorkManager de l'exécuter. Il existe deux types de WorkRequest :

  • OneTimeWorkRequest : WorkRequest qui ne s'exécute qu'une seule fois.
  • PeriodicWorkRequest : WorkRequest qui s'exécute de manière répétée dans un cycle.

L'image ne doit être floutée qu'une seule fois lorsque vous cliquez sur le bouton Start (Démarrer).

Cette tâche s'effectue dans la méthode applyBlur(), que vous appelez lorsque vous cliquez sur le bouton Start (Démarrer).

Les étapes suivantes ont lieu dans la méthode applyBlur().

  1. Renseignez une nouvelle variable nommée blurBuilder en créant une OneTimeWorkRequest pour le worker de floutage et en appelant la fonction d'extension OneTimeWorkRequestBuilder de WorkManager KTX.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
}
  1. Démarrez la tâche en appelant la méthode enqueue() au niveau de l'objet workManager.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // Start the work
    workManager.enqueue(blurBuilder.build())
}
  1. Exécutez l'application et consultez la notification qui s'affiche lorsque vous cliquez sur le bouton Start (Démarrer).

Pour le moment, l'image est floutée de la même manière, quelle que soit l'option sélectionnée. Lors des étapes suivantes, le niveau de flou change en fonction de l'option sélectionnée.

f2b3591b86d1999d.png

Pour vérifier que l'image a bien été floutée, vous pouvez ouvrir Device Explorer (Explorateur de l'appareil) dans Android Studio :

6bc555807e67f5ad.png

Accédez ensuite à data > data > com.example.bluromatic > files > blur_filter_outputs > <URI> et vérifiez que l'image de cupcake est bien floutée :

fce43c920a61a2e3.png

10. Données d'entrée et de sortie

Le floutage de l'élément image dans le répertoire de ressources est satisfaisant, mais pour que Blur-O-Matic soit vraiment l'application révolutionnaire de retouche d'images qu'elle prétend devenir, vous devez laisser l'utilisateur flouter l'image qu'il voit à l'écran, puis afficher le résultat flouté.

Pour ce faire, nous fournissons l'URI de l'image de cupcake affichée en entrée à notre WorkRequest, puis nous utilisons la sortie de cette WorkRequest pour afficher l'image floutée finale.

ce8ec44543479fe5.png

Les entrées et les sorties sont transmises vers et depuis un worker via un objet Data. Les objets Data sont des conteneurs légers pour les paires clé/valeur. Ils servent à stocker une petite quantité de données qui peuvent être transmises à un worker en entrée ou en sortie à partir de la WorkRequest.

À l'étape suivante, vous transmettrez l'URI à BlurWorker en créant un objet de données d'entrée.

Créer un objet de données d'entrée

  1. Dans le fichier data/WorkManagerBluromaticRepository.kt, au sein de la classe WorkManagerBluromaticRepository, créez une variable privée nommée imageUri.
  2. Renseignez la variable avec l'URI de l'image en appelant la méthode de contexte getImageUri().

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.getImageUri
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    private var imageUri: Uri = context.getImageUri() // <- Add this
    private val workManager = WorkManager.getInstance(context)
...

Le code de l'application contient la fonction d'assistance createInputDataForWorkRequest() qui permet de créer des objets de données d'entrée.

data/WorkManagerBluromaticRepository.kt

// For reference - already exists in the app
private fun createInputDataForWorkRequest(blurLevel: Int, imageUri: Uri): Data {
    val builder = Data.Builder()
    builder.putString(KEY_IMAGE_URI, imageUri.toString()).putInt(BLUR_LEVEL, blurLevel)
    return builder.build()
}

La fonction d'assistance crée d'abord un objet Data.Builder. Elle y insère ensuite l'imageUri et le blurLevel sous la forme de paires clé/valeur. Puis, un objet de données est créé et renvoyé lorsque return builder.build() est appelé.

  1. Pour définir l'objet de données d'entrée pour une requête de travail, appelez la méthode blurBuilder.setInputData(). Pour créer et transmettre l'objet de données en une seule étape, vous pouvez appeler la fonction d'assistance createInputDataForWorkRequest() en tant qu'argument. Pour l'appel de la fonction createInputDataForWorkRequest(), transmettez les variables blurLevel et imageUri.

data/WorkManagerBluromaticRepository.kt

override fun applyBlur(blurLevel: Int) {
     // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // New code for input data object
    blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))

    workManager.enqueue(blurBuilder.build())
}

Accéder à l'objet de données d'entrée

Nous allons maintenant mettre à jour la méthode doWork() de la classe BlurWorker pour obtenir l'URI et le niveau de flou transmis par l'objet de données d'entrée. Si aucune valeur n'a été spécifiée pour blurLevel, la valeur par défaut est 1.

Dans la méthode doWork() :

  1. Créez une variable nommée resourceUri et renseignez-la en appelant inputData.getString() et en transmettant la constante KEY_IMAGE_URI qui a été utilisée comme clé lors de la création de l'objet de données d'entrée.

val resourceUri = inputData.getString(KEY_IMAGE_URI)

  1. Créez une variable nommée blurLevel. Renseignez la variable en appelant inputData.getInt() et en transmettant la constante BLUR_LEVEL qui a été utilisée comme clé lors de la création de l'objet de données d'entrée. Si cette paire clé/valeur n'a pas été créée, fournissez la valeur par défaut 1 (un).

workers/BlurWorker.kt.

import com.example.bluromatic.KEY_BLUR_LEVEL
import com.example.bluromatic.KEY_IMAGE_URI
...
override fun doWork(): Result {

    // ADD THESE LINES
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    val blurLevel = inputData.getInt(KEY_BLUR_LEVEL, 1)

    // ... rest of doWork()
}

Avec l'URI, floutons maintenant l'image de cupcake à l'écran.

  1. Vérifiez que la variable resourceUri est renseignée. Si ce n'est pas le cas, le code devrait générer une exception. Le code ci-dessous utilise l'instruction require() qui génère une exception IllegalArgumentException si le premier argument est "false".

workers/BlurWorker.kt

return@withContext try {
    // NEW code
    require(!resourceUri.isNullOrBlank()) {
        val errorMessage =
            applicationContext.resources.getString(R.string.invalid_input_uri)
            Log.e(TAG, errorMessage)
            errorMessage
    }

Étant donné que la source de l'image est transmise en tant qu'URI, nous avons besoin d'un objet ContentResolver pour lire le contenu vers lequel renvoie l'URI.

  1. Ajoutez un objet contentResolver à la valeur applicationContext.

workers/BlurWorker.kt.

...
    require(!resourceUri.isNullOrBlank()) {
        // ...
    }
    val resolver = applicationContext.contentResolver
...
  1. Étant donné que la source de l'image est maintenant transmise dans l'URI, utilisez BitmapFactory.decodeStream() au lieu de BitmapFactory.decodeResource() pour créer l'objet Bitmap.

workers/BlurWorker.kt.

import android.net.Uri
...
//     val picture = BitmapFactory.decodeResource(
//         applicationContext.resources,
//         R.drawable.android_cupcake
//     )

    val resolver = applicationContext.contentResolver

    val picture = BitmapFactory.decodeStream(
        resolver.openInputStream(Uri.parse(resourceUri))
    )
  1. Transmettez la variable blurLevel dans l'appel de la fonction blurBitmap().

workers/BlurWorker.kt

//val output = blurBitmap(picture, 1)
val output = blurBitmap(picture, blurLevel)

Créer un objet de données de sortie

Vous en avez terminé avec ce worker et pouvez renvoyer l'URI de sortie en tant qu'objet de données de sortie dans Result.success(). Fournir l'URI de sortie en tant qu'objet de données de sortie permet aux autres workers d'y accéder facilement pour d'autres opérations. Cette approche sera utile dans la section suivante, lorsque vous créerez une chaîne de workers.

Voici l'approche à suivre :

  1. Avant le code Result.success(), créez une variable nommée outputData.
  2. Renseignez cette variable en appelant la fonction workDataOf() et utilisez la constante KEY_IMAGE_URI pour la clé et la variable outputUri comme valeur. La fonction workDataOf() crée un objet de données à partir de la paire clé/valeur transmise.

workers/BlurWorker.kt.

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
  1. Mettez à jour le code Result.success() pour utiliser ce nouvel objet de données comme argument.

workers/BlurWorker.kt

//Result.success()
Result.success(outputData)
  1. Supprimez le code qui affiche la notification. Il n'est plus nécessaire, car l'objet de données de sortie utilise désormais l'URI.

workers/BlurWorker.kt

// REMOVE the following notification code
//makeStatusNotification(
//    "Output is $outputUri",
//    applicationContext
//)

Exécuter votre application

À ce stade, lorsque vous exécutez votre application, la compilation devrait avoir lieu. Vous pouvez voir l'image floutée dans Device Explorer (Explorateur de l'appareil), mais pas encore à l'écran.

Notez que vous devrez peut-être lancer la synchronisation pour afficher les images :

a658ad6e65f0ce5d.png

Beau travail ! Vous avez flouté une image d'entrée en utilisant WorkManager.

11. Créer votre chaîne

Pour l'instant, vous n'effectuez qu'une seule tâche : flouter l'image. Bien que cette première réalisation soit essentielle, certaines fonctionnalités de base doivent encore être ajoutées dans l'application :

  • L'application ne nettoie pas les fichiers temporaires.
  • L'application n'enregistre pas l'image dans un fichier permanent.
  • L'application floute toujours l'image de la même manière.

Vous pouvez utiliser une chaîne de travail WorkManager pour ajouter cette fonctionnalité. WorkManager vous permet de créer des WorkerRequest distinctes qui s'exécutent de façon séquentielle ou parallèle.

Dans cette section, vous allez créer une chaîne de travail qui se présente comme suit :

c883bea5a5beac45.png

Les cases représentent les WorkRequests.

Une chaîne de travail présente également l'avantage d'accepter les entrées et de générer des sorties. La sortie d'une WorkRequest devient l'entrée de la WorkRequest suivante dans la chaîne.

Vous disposez déjà d'un CoroutineWorker pour flouter une image, mais vous avez également besoin d'un CoroutineWorker qui nettoie les fichiers temporaires et d'un CoroutineWorker qui enregistre l'image de manière définitive.

Créer un worker de nettoyage

La classe CleanupWorker supprime les fichiers temporaires.

  1. Effectuez un clic droit sur le package com.example.bluromatic.workers dans le volet de votre projet Android, puis sélectionnez New -> Kotlin Class/File (Nouveau -> Classe/Fichier Kotlin).
  2. Nommez la nouvelle classe Kotlin CleanupWorker.
  3. Copiez le code de CleanupWorker.kt, comme illustré dans l'exemple de code suivant.

Étant donné que la manipulation des fichiers n'entre pas dans le cadre de cet atelier de programmation, vous pouvez copier le code suivant pour CleanupWorker.

workers/CleanupWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.OUTPUT_PATH
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"

class CleanupWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {
        /** Makes a notification when the work starts and slows down the work so that it's easier
         * to see each WorkRequest start, even on emulated devices
         */
        makeStatusNotification(
            applicationContext.resources.getString(R.string.cleaning_up_files),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

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

Créer un worker d'enregistrement de l'image dans un fichier

La classe SaveImageToFileWorker enregistre le fichier temporaire dans un fichier permanent.

SaveImageToFileWorker accepte les entrées et les sorties. L'entrée est une String de l'URI de l'image floutée temporairement. Elle est stockée avec la clé KEY_IMAGE_URI. La sortie est une String de l'URI de l'image floutée enregistrée. Elle est stockée avec la clé KEY_IMAGE_URI.

de0ee97cca135cf8.png

  1. Effectuez un clic droit sur le package com.example.bluromatic.workers dans le volet de votre projet Android, puis sélectionnez New -> Kotlin Class/File (Nouveau -> Classe/Fichier Kotlin).
  2. Nommez la nouvelle classe Kotlin SaveImageToFileWorker.
  3. Copiez le code de SaveImageToFileWorker.kt, comme indiqué dans l'exemple de code suivant.

Étant donné que la manipulation des fichiers n'entre pas dans le cadre de cet atelier de programmation, vous pouvez copier le code suivant pour SaveImageToFileWorker. Dans le code fourni, notez que les valeurs resourceUri et output sont récupérées et stockées avec la clé KEY_IMAGE_URI. Ce processus est très semblable au code que vous avez écrit précédemment pour les objets de données d'entrée et de sortie.

workers/SaveImageToFileWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.KEY_IMAGE_URI
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"

class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    private val title = "Blurred Image"
    private val dateFormatter = SimpleDateFormat(
        "yyyy.MM.dd 'at' HH:mm:ss z",
        Locale.getDefault()
    )

    override suspend fun doWork(): Result {
        // Makes a notification when the work starts and slows down the work so that
        // it's easier to see each WorkRequest start, even on emulated devices
        makeStatusNotification(
            applicationContext.resources.getString(R.string.saving_image),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

            val resolver = applicationContext.contentResolver
            return@withContext try {
                val resourceUri = inputData.getString(KEY_IMAGE_URI)
                val bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri))
                )
                val imageUrl = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, title, dateFormatter.format(Date())
                )
                if (!imageUrl.isNullOrEmpty()) {
                    val output = workDataOf(KEY_IMAGE_URI to imageUrl)

                    Result.success(output)
                } else {
                    Log.e(
                        TAG,
                        applicationContext.resources.getString(R.string.writing_to_mediaStore_failed)
                    )
                    Result.failure()
                }
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_saving_image),
                    exception
                )
                Result.failure()
            }
        }
    }
}

Créer une chaîne de travail

Actuellement, le code ne crée et n'exécute qu'une seule WorkRequest.

Au cours de cette étape, vous allez modifier le code pour créer et exécuter une chaîne de requêtes de travail au lieu d'une seule requête de floutage d'image.

Dans la chaîne de requêtes de travail, la première requête consistera à nettoyer les fichiers temporaires.

  1. Au lieu d'appeler OneTimeWorkRequestBuilder, appelez workManager.beginWith().

L'appel de la méthode beginWith() renvoie un objet WorkContinuation et crée la première requête d'une chaîne de WorkRequests comme point de départ.

data/WorkManagerBluromaticRepository.kt

import androidx.work.OneTimeWorkRequest
import com.example.bluromatic.workers.CleanupWorker
// ...
    override fun applyBlur(blurLevel: Int) {
        // Add WorkRequest to Cleanup temporary images
        var continuation = workManager.beginWith(OneTimeWorkRequest.from(CleanupWorker::class.java))

        // Add WorkRequest to blur the image
        val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
...

Pour ajouter des requêtes à cette chaîne de requêtes de travail, appelez la méthode then() et transmettez un objet WorkRequest.

  1. Supprimez l'appel de workManager.enqueue(blurBuilder.build()), qui mettait une seule requête de travail en file d'attente.
  2. Ajoutez la requête de travail suivante à la chaîne en appelant la méthode .then().

data/WorkManagerBluromaticRepository.kt

...
//workManager.enqueue(blurBuilder.build())

// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
  1. Créez une requête de travail pour enregistrer l'image, puis ajoutez-la à la chaîne.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.SaveImageToFileWorker

...
continuation = continuation.then(blurBuilder.build())

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .build()
continuation = continuation.then(save)
...
  1. Pour commencer le travail, appelez la méthode enqueue() au niveau de l'objet de continuation.

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

// Start the work
continuation.enqueue()
...

Ce code génère et exécute la chaîne de requêtes de travail suivante : une WorkRequest CleanupWorker suivie d'une WorkRequest BlurWorker suivie d'une WorkRequest SaveImageToFileWorker.

  1. Exécutez l'application.

Vous pouvez maintenant cliquer sur Start (Démarrer) et voir les notifications lorsque les différents workers s'exécutent. Vous continuez à voir l'image floutée dans Device Explorer (Explorateur de l'appareil). Dans une section à venir, vous ajouterez un bouton afin que les utilisateurs puissent voir l'image floutée sur l'appareil.

Dans les captures d'écran suivantes, le message de notification affiche le worker en cours d'exécution.

bbe0fdd79e3bca27.png

5d43bbfff1bfebe5.png

da2d31fa3609a7b1.png

Notez que le dossier de sortie contient plusieurs images floutées : les images qui sont dans des étapes floues intermédiaires et l'image finale qui affiche le niveau de flou sélectionné.

Bravo ! Vous pouvez maintenant nettoyer les fichiers temporaires, flouter une image et l'enregistrer :

12. 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 intermediate

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.

13. Conclusion

Félicitations ! Vous avez terminé l'application Blur-O-Matic. Vous en savez maintenant plus sur les points suivants :

  • Ajout de WorkManager à votre projet
  • Planification d'une OneTimeWorkRequest
  • Paramètres d'entrée et de sortie
  • Enchaînement de tâches et de WorkRequest

WorkManager est compatible avec bien d'autres fonctionnalités que nous n'avons pu traiter dans cet atelier de programmation, comme les tâches répétitives, la bibliothèque Support de test, les requêtes de travail parallèles et les fusions d'entrées.

Pour en savoir plus, consultez la documentation Planifier des tâches avec WorkManager.