Travail en arrière-plan avec WorkManager – Kotlin

1. Présentation

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

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 votre application.

WorkManager est une bibliothèque incroyablement flexible, qui présente de nombreux avantages. En voici un aperçu :

  • 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, y compris l'exécution du travail 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
  • Compatibilité avec LiveData, pour afficher facilement l'état des requêtes de travail dans l'interface utilisateur

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'écran ou l'application en question.

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

  • Envoi de journaux
  • Application de filtres à des images et enregistrement de celles-ci
  • Synchronisation périodique des données locales avec le réseau

WorkManager garantit l'exécution, ce que n'exigent pas toutes les tâches. De ce fait, ce n'est pas un outil générique pour exécuter n'importe quelle tâche en dehors du thread principal. Pour savoir quand utiliser WorkManager, consultez le guide sur le traitement en arrière-plan.

Ce que vous allez faire

Les smartphones actuels sont presque trop bons pour prendre des photos. Le temps où le photographe pouvait prendre une photo un peu floue d'un objet 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 le résultat 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.

Image de l'application terminée, avec une image d'espace réservé de cupcake, trois options de floutage pour l'image et deux boutons. Un permettant de flouter l'image et un autre permettant de la visualiser.

Image floutée telle qu'elle s'affiche une fois que vous avez cliqué sur "Consulter le fichier".

Ce que vous allez apprendre

  • Ajout de WorkManager à votre projet
  • Planification d'une tâche simple
  • Paramètres d'entrée et de sortie
  • Enchaînement de travaux
  • Travail unique
  • Affichage de l'état du travail dans l'interface utilisateur
  • Annulation d'un travail
  • Contraintes liées aux travaux

Prérequis

2. Configuration

Étape 1 : Téléchargez 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 l'atelier de programmation sur WorkManager depuis GitHub :

$ git clone -b start_kotlin https://github.com/googlecodelabs/android-workmanager

Étape 2 : Exécutez l'application

Exécutez l'application. L'écran suivant devrait s'afficher :

9e4707e0fbdd93c7.png

Des cases d'option permettent de sélectionner le niveau de flou de l'image. Appuyez sur le bouton Go (Appliquer) pour flouter et enregistrer l'image.

À ce stade, l'application n'applique aucun floutage.

Le code de départ contient les éléments suivants :

  • WorkerUtils : cette classe contient le code du floutage d'image, ainsi que des méthodes pratiques que vous utiliserez plus tard pour afficher Notifications, enregistrer un fichier bitmap et ralentir l'application.
  • BlurActivity :* activité qui permet d'afficher l'image, et qui inclut des cases d'option pour sélectionner le niveau de flou.
  • BlurViewModel :* ce modèle de vue stocke toutes les données nécessaires à l'affichage de BlurActivity. C'est également dans cette classe que vous démarrez le travail en arrière-plan à l'aide de WorkManager.
  • Constants : classe statique comprenant quelques constantes que vous utiliserez dans cet atelier de programmation.
  • res/activity_blur.xml : fichiers de mise en page pour BlurActivity.

***** Seuls fichiers dans lesquels vous écrirez le code.

3. Ajouter WorkManager à votre application

WorkManager nécessite la dépendance Gradle ci-dessous. Ils ont déjà été inclus dans les fichiers de version :

app/build.gradle

dependencies {
    // WorkManager dependency
    implementation "androidx.work:work-runtime-ktx:$versions.work"
}

Vous devez télécharger la dernière version stable de work-runtime-ktx sur cette page et installer la version appropriée. À l'heure actuelle, la dernière version est la suivante :

build.gradle

versions.work = "2.7.1"

Si vous passez à une version plus récente, veillez à cliquer sur Sync Now (Synchroniser maintenant) pour synchroniser votre projet avec les fichiers Gradle modifiés.

4. Créer votre première WorkRequest

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. Celles-ci vont flouter l'image et l'enregistrer dans un fichier temporaire.

Principes de base de WorkManager

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

  • Worker : c'est ici que vous devez insérer le code correspondant au travail à effectuer en arrière-plan. Vous allez développer cette classe et remplacer la méthode doWork().
  • WorkRequest : représente une requête d'exécution d'un travail. Vous transmettrez votre Worker dans le cadre de la création de votre WorkRequest. Lorsque vous créez la WorkRequest, vous pouvez aussi spécifier des éléments tels que des Constraints concernant l'exécution de Worker.
  • WorkManager : cette classe planifie votre WorkRequest et l'exécute. Elle planifie les WorkRequest de manière à répartir la charge pesant sur les ressources système, tout en respectant les contraintes que vous spécifiez.

Dans le cas présent, vous devrez définir un nouveau BlurWorker, qui contiendra le code permettant de flouter une image. Lorsque vous cliquez sur le bouton Go (Appliquer), WorkRequest est créé, puis placé en file d'attente par WorkManager.

Étape 1 : Créez BlurWorker

Dans le package workers, créez une classe Kotlin appelée BlurWorker.

Étape 2 : Ajoutez un constructeur

Ajoutez une dépendance à Worker pour la classe BlurWorker :

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

Étape 3 : Remplacez et implémentez doWork()

Votre Worker floutera l'image de cupcake affichée.

Pour déterminer au mieux le moment d'exécution, vous allez utiliser la propriété makeStatusNotification() de WorkerUtil. Cette méthode vous permet d'afficher facilement une bannière de notification en haut de l'écran.

Remplacez la méthode doWork(), puis implémentez les éléments suivants. Reportez-vous au code final à la fin de la section :

  1. Obtenez un Context en appelant la propriété applicationContext. Attribuez-le à un nouveau val nommé appContext. Vous en aurez besoin pour les diverses manipulations bitmap que vous êtes sur le point d'effectuer.
  2. Affichez une notification d'état à l'aide de la fonction makeStatusNotification pour avertir l'utilisateur du floutage de l'image.
  3. Créez un Bitmap à partir de l'image de cupcake :
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.android_cupcake)
  1. Obtenez une version floue du bitmap en appelant la méthode blurBitmap à partir de WorkerUtils.
  2. Écrivez ce bitmap dans un fichier temporaire en appelant la méthode writeBitmapToFile à partir de WorkerUtils. Veillez à enregistrer l'URI renvoyé dans une variable locale.
  3. Créez une notification affichant l'URI en appelant la méthode makeStatusNotification à partir de WorkerUtils.
  4. Renvoyez Result.success().
  5. Encapsulez le code des étapes 3 à 6 dans une instruction try/catch. Récupérez un Throwable générique.
  6. Dans l'instruction catch, affichez un message d'erreur à l'aide de l'instruction de journalisation : Log.e(TAG, "Error applying blur").
  7. Dans l'instruction catch, renvoyez ensuite Result.failure().

Le code final de cette étape est donné ci-dessous.

**BlurWorker.**kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R

private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override fun doWork(): Result {
        val appContext = applicationContext

        makeStatusNotification("Blurring image", appContext)

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

            val output = blurBitmap(picture, appContext)

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

            makeStatusNotification("Output is $outputUri", appContext)

            Result.success()
        } catch (throwable: Throwable) {
            Log.e(TAG, "Error applying blur")
            Result.failure()
        }
    }
}

Étape 4 : Obtenez WorkManager dans ViewModel

Créez une variable de classe pour une instance WorkManager dans votre ViewModel :

BlurViewModel.kt

private val workManager = WorkManager.getInstance(application)

Étape 5 : Placez WorkRequest en file d'attente dans WorkManager

Il est temps de créer une 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 cyclique.

L'image ne doit être floutée qu'une seule fois, lorsque vous cliquez sur le bouton Go (Appliquer). La méthode applyBlur est appelée quand vous cliquez sur le bouton Go (Appliquer). Vous devez donc créer une OneTimeWorkRequest à partir de BlurWorker. Ensuite, utilisez votre instance WorkManager pour placer votre WorkRequest. en file d'attente.

Ajoutez la ligne de code suivante à la méthode BlurViewModel's applyBlur() :

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
   workManager.enqueue(OneTimeWorkRequest.from(BlurWorker::class.java))
}

Étape 6 : Exécutez le code

Exécutez votre code. Il devrait se compiler, et une notification devrait s'afficher lorsque vous cliquez sur le bouton Go (Appliquer). Notez que pour obtenir un résultat plus flou, vous devez sélectionner l'option "More blurred" (Plus flouté) ou "The most blurred" (Le plus flouté).

ed497b57e1f527be.png

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

cf10a1af6e84f5ff.png

Accédez ensuite à data > data > com.example.background > files > blur_filter_outputs > <URI>, puis vérifiez que le cupcake a bien été flouté :

e1f61035d680ba03.png

5. Ajouter une entrée et une sortie

Le floutage de l'élément image dans le répertoire de ressources est satisfaisant, mais pour que Blur-O-Matic révèle son plein potentiel en tant qu'application de retouche d'images, vous devez permettre à l'utilisateur de flouter l'image qui s'affiche à l'écran, puis lui montrer le résultat flouté.

Pour ce faire, nous allons fournir l'URI de l'image de cupcake en tant qu'entrée à notre WorkRequest qui s'affiche, puis utiliser la sortie de notre WorkRequest pour montrer l'image floutée finale.

Étape 1 : Créez un objet d'entrée de données

Les entrées et les sorties sont transférées via des objets 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 ou non depuis des WorkRequest.

Vous allez transmettre l'URI de l'image de l'utilisateur à un groupe. Cet URI est stocké dans une variable appelée imageUri.

Dans BlurViewModel, créez une méthode privée appelée createInputDataForUri. Cette méthode doit permettre d'effectuer les tâches suivantes :

  1. Créez un objet Data.Builder. Importez androidx.work.Data, si nécessaire.
  2. Si imageUri est une valeur URI non nulle, ajoutez-le à l'objet Data à l'aide de la méthode putString. Cette méthode utilise une clé et une valeur. Vous pouvez utiliser la constante de chaîne KEY_IMAGE_URI depuis la classe Constants.
  3. Appeler build() sur l'objet Data.Builder pour créer l'objet Data et le renvoyer.

Vous trouverez ci-dessous la méthode createInputDataForUri complète :

BlurViewModel.kt

/**
 * Creates the input data bundle which includes the Uri to operate on
 * @return Data which contains the Image Uri as a String
 */
private fun createInputDataForUri(): Data {
    val builder = Data.Builder()
    imageUri?.let {
        builder.putString(KEY_IMAGE_URI, imageUri.toString())
    }
    return builder.build()
}

Étape 2 : Transmettez l'objet de données à WorkRequest

Vous allez modifier la méthode applyBlur dans BlurViewModel de sorte qu'il :

  1. crée un objet OneTimeWorkRequestBuilder ;
  2. appelle setInputData et transmette le résultat depuis createInputDataForUri ;
  3. crée OneTimeWorkRequest ;
  4. Met la requête en file d'attente à l'aide de la requête WorkManager afin que l'exécution du travail soit planifiée.

Vous trouverez ci-dessous la méthode applyBlur complète :

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
            .setInputData(createInputDataForUri())
            .build()

    workManager.enqueue(blurRequest)
}

Étape 3 : Mettez à jour l'élément "doWork()" de BlurWorker pour obtenir l'entrée

Nous allons maintenant mettre à jour la méthode doWork() de BlurWorker pour obtenir l'URI que nous avons transmis à partir de l'objet Data :

BlurWorker.kt

override fun doWork(): Result {
    val appContext = applicationContext

    // ADD THIS LINE
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    // ... rest of doWork()
}

Étape 4 : Floutez l'URI donné

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

  1. Supprimez le code précédent qui obtient la ressource image.

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

  1. Vérifiez que l'URI resourceUri obtenu à partir des données (Data) transmises n'est pas vide.
  2. Attribuez la variable picture à l'image transmise, comme suit :

val picture = BitmapFactory.decodeStream(

appContext.contentResolver.

  `openInputStream(Uri.parse(resourceUri)))`

BlurWorker.kt

override fun doWork(): Result {
    val appContext = applicationContext

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    return try {
        // REMOVE THIS
        //    val picture = BitmapFactory.decodeResource(
        //            appContext.resources,
        //            R.drawable.android_cupcake)

        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

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

        Result.success()
    } catch (throwable: Throwable) {
        Log.e(TAG, "Error applying blur")
        throwable.printStackTrace()
        Result.failure()
    }
}

Étape 5 : Renvoyez l'URI temporaire

Vous avez terminé avec ce nœud de calcul et pouvez renvoyer l'URI de sortie dans Result.success(). Indiquez l'URI de sortie en tant que donnée de sortie pour que cette image temporaire soit facilement accessible aux autres nœuds de calcul pour d'autres opérations. Cela servira lorsque nous créerons une chaîne de nœuds de calcul au prochain chapitre. Pour cela :

  1. Créez un fichier Data, comme vous l'avez fait pour l'entrée, puis enregistrez outputUri en tant que String. Utilisez la même clé, KEY_IMAGE_URI.
  2. Renvoyez-la à WorkManager à l'aide de la méthode Result.success(Data outputData).

BlurWorker.kt

Remplacez la ligne Result.success() de doWork() par :

val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

Result.success(outputData)

Étape 6 : Exécutez votre application

À ce stade, vous devez exécuter votre application. Elle doit se compiler et présenter le même comportement que lorsque vous voyez l'image floutée dans l'Explorateur de fichiers de l'appareil, même si ce n'est pas encore le cas à l'écran.

Pour voir une autre image floue, vous pouvez ouvrir l'Explorateur de fichiers de l'appareil dans Android Studio et accéder à data/data/com.example.background/files/blur_filter_outputs/<URI> comme à l'étape précédente.

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

7e717ffd6b3d9d52.png

Bien joué ! Vous avez flouté une image d'entrée en utilisant WorkManager.

6. Créer votre chaîne

Pour l'instant, vous n'effectuez qu'une seule tâche : flouter l'image. C'est une première étape importante, mais il manque quelques fonctionnalités de base :

  • Les fichiers temporaires ne sont pas nettoyés.
  • L'image n'est pas enregistrée dans un fichier permanent.
  • L'image est toujours floutée de la même manière.

Nous allons 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. Au cours de cette étape, vous allez créer une chaîne de travail qui ressemble à ceci :

54832b34e9c9884a.png

Les WorkRequest sont représentées par des cases.

Autre avantage très intéressant de la création d'une chaîne : la sortie d'une WorkRequest est l'entrée de la WorkRequest suivante dans la chaîne. L'entrée et la sortie transmises entre chaque WorkRequest s'affichent en bleu.

Étape 1 : Configurez le nettoyage et enregistrez les nœuds de calcul

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

Créez deux classes dans le package workers qui étendent Worker.

La première doit être appelée CleanupWorker, et la seconde SaveImageToFileWorker.

Étape 2 : Étendez le nœud de calcul

Étendez la classe CleanupWorker depuis la classe Worker. Ajoutez les paramètres de constructeur requis.

class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}

Étape 3 : Remplacez et implémentez doWork() pour CleanupWorker

CleanupWorker n'a pas besoin de recevoir d'entrées ni de transmettre des sorties. Il supprime toujours les fichiers temporaires existants. Comme nous n'allons pas aborder la manipulation de fichiers dans cet atelier de programmation, vous pouvez copier le code de CleanupWorker ci-dessous :

CleanupWorker.kt

package com.example.background.workers

import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override 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("Cleaning up old temporary files", applicationContext)
        sleep()

        return 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) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

Étape 4 : Remplacez et implémentez doWork() pour SaveImageToFileWorker

SaveImageToFileWorker nécessite une entrée et transmet une sortie. L'entrée est une String de l'URI de l'image floutée temporairement stockée avec la clé KEY_IMAGE_URI. La sortie sera également une String, l'URI de l'image floutée enregistrée stockée avec la clé KEY_IMAGE_URI.

4fc29ac70fbecf85.png

Comme il ne s'agit toujours pas d'un atelier de programmation sur les manipulations de fichiers, le code est fourni ci-dessous. Notez la façon dont les valeurs resourceUri et output sont récupérées avec la clé KEY_IMAGE_URI. Ce code est très semblable à celui que vous avez écrit à l'étape précédente pour l'entrée et la sortie (il utilise les mêmes clés).

SaveImageToFileWorker.kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

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

    override 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("Saving image", applicationContext)
        sleep()

        val resolver = applicationContext.contentResolver
        return 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, "Writing to MediaStore failed")
                Result.failure()
            }
        } catch (exception: Exception) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

Étape 5 : Modifiez la notification BlurWorker

Maintenant que nous disposons d'une chaîne de Worker qui se charge d'enregistrer l'image dans le dossier approprié, nous pouvons ralentir le travail à l'aide de la méthode sleep() définie dans la classe WorkerUtils, afin que l'utilisateur puisse voir plus facilement chaque WorkRequest démarrer, même sur les appareils émulés. La version finale de BlurWorker devient :

BlurWorker.kt

class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

override fun doWork(): Result {
    val appContext = applicationContext

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    // ADD THIS TO SLOW DOWN THE WORKER
    sleep()
    // ^^^^

    return try {
        if (TextUtils.isEmpty(resourceUri)) {
            Timber.e("Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

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

        val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

        Result.success(outputData)
    } catch (throwable: Throwable) {
        throwable.printStackTrace()
        Result.failure()
    }
}

Étape 6 : Créez une chaîne de WorkRequest

Vous devez modifier la méthode applyBlur de BlurViewModel afin qu'elle exécute une chaîne de WorkRequest au lieu d'un seul. Actuellement, le code ressemble à ceci :

BlurViewModel.kt

val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
        .setInputData(createInputDataForUri())
        .build()

workManager.enqueue(blurRequest)

Au lieu d'appeler workManager.enqueue(), appelez workManager.beginWith(). Cela renvoie une WorkContinuation, qui définit une chaîne de WorkRequest. Vous pouvez ajouter des éléments à cette chaîne de requêtes de travail en appelant la méthode then(). Par exemple, si vous avez trois objets WorkRequest, workA, workB et workC, vous pouvez procéder comme suit :

// Example code, don't copy to the project
val continuation = workManager.beginWith(workA)

continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
        .then(workC)
        .enqueue() // Enqueues the WorkContinuation which is a chain of work

Ce code génère et exécute la chaîne de WorkRequests suivante :

bf3b82eb9fd22349.png

Créez une chaîne de CleanupWorker WorkRequest, une BlurImage WorkRequest et une SaveImageToFile WorkRequest dans applyBlur. Transmettez l'entrée à BlurImage WorkRequest.

Le code correspondant est donné ci-dessous :

BlurViewModel.kt

internal 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 blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)
            .setInputData(createInputDataForUri())
            .build()

    continuation = continuation.then(blurRequest)

    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequest.Builder(SaveImageToFileWorker::class.java).build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

Le code devrait se compiler et s'exécuter. Vous devriez maintenant pouvoir appuyer sur le bouton Go (Appliquer) et voir les notifications lors de l'exécution des différents nœuds de calcul. Vous pourrez toujours voir l'image floutée dans l'Explorateur de fichiers de l'appareil. Au cours d'une prochaine étape, vous allez ajouter un bouton supplémentaire afin que les utilisateurs puissent voir l'image floutée sur l'appareil.

Comme vous pouvez le voir dans les captures d'écran ci-dessous, les messages de notification indiquent quel nœud de calcul est en cours d'exécution.

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

Étape 7 : Exécutez à nouveau BlurWorker

Il est maintenant temps d'ajouter la possibilité de flouter plus ou moins fortement l'image. Prenez le paramètre blurLevel transmis à applyBlur et ajoutez autant d'opérations WorkRequest de floutage à la chaîne. Seules les premières WorkRequest ont besoin de recevoir l'entrée URI.

Essayez, puis comparez avec le code ci-dessous :

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    // Add WorkRequest to Cleanup temporary images
    var continuation = workManager
            .beginWith(OneTimeWorkRequest
            .from(CleanupWorker::class.java))

    // Add WorkRequests to blur the image the number of times requested
    for (i in 0 until blurLevel) {
        val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

        // Input the Uri if this is the first blur operation
        // After the first blur operation the input will be the output of previous
        // blur operations.
        if (i == 0) {
            blurBuilder.setInputData(createInputDataForUri())
        }

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

    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
            .build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

Ouvrez l'Explorateur de fichiers de l'appareil pour afficher les images floutées. Notez que le dossier de sortie contient plusieurs images floutées, celles qui se trouvent aux étapes intermédiaires du floutage et l'image finale qui affiche l'image floutée en fonction du niveau de flou sélectionné.

Beau travail ! Vous pouvez désormais modifier le niveau de floutage d'une image pour plus de mystère !

7. Assurer un travail unique

Maintenant que vous avez utilisé des chaînes, il est temps de voir une autre fonctionnalité puissante de WorkManager : les chaînes de travail uniques.

Il arrive parfois 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. Vous indiquez un nom String unique. Ce nom qualifie l'intégralité de la chaîne de requêtes de travail. Ainsi, vous pouvez y faire référence et les interroger ensemble.

Assurez-vous que votre chaîne de travail pour flouter votre fichier est unique en utilisant beginUniqueWork. Transmettez IMAGE_MANIPULATION_WORK_NAME comme clé. Vous devez également transmettre une ExistingWorkPolicy. Les options disponibles sont REPLACE, KEEP et APPEND.

Vous utiliserez REPLACE, car nous voulons arrêter le floutage en cours si l'utilisateur décide de flouter une nouvelle image avant la fin.

Le code permettant de démarrer votre continuation de travail unique est le suivant :

BlurViewModel.kt

// 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 va désormais flouter qu'une image à la fois.

8. Ajouter un tag et afficher l'état du travail

Cette section fait un usage intensif de LiveData. Ainsi, pour bien comprendre ce qui se passe, vous devez connaître LiveData. LiveData est un conteneur de données observable et sensible au cycle de vie.

Si c'est la première fois que vous travaillez avec LiveData ou des objets observables, consultez la documentation ou suivez l'atelier de programmation sur les composants du cycle de vie d'Android.

La modification majeure suivante consiste à modifier ce qui s'affiche dans l'application au fur et à mesure que le travail s'exécute.

Vous pouvez obtenir l'état de n'importe quelle WorkRequest en obtenant un LiveData contenant un objet WorkInfo. WorkInfo est un objet qui contient des informations sur l'état actuel d'une WorkRequest, y compris :

Le tableau suivant présente trois méthodes différentes permettant d'obtenir des objets LiveData<WorkInfo> ou LiveData<List<WorkInfo>>, ainsi que leur fonction.

Type

Méthode WorkManager

Description

Obtention du travail à partir de l'ID

getWorkInfoByIdLiveData

Chaque WorkRequest possède un ID unique généré par WorkManager. Vous pouvez utiliser cette valeur pour obtenir un seul LiveData
pour la WorkRequest en question.

Obtention du travail à partir d'un nom de chaîne unique

getWorkInfosForUniqueWorkLiveData

Comme vous venez de le voir, les WorkRequest peuvent faire partie d'une chaîne unique. Celle-ci renvoie LiveData
>
pour toutes les travaux d'une chaîne unique de WorkRequests.

Obtention du travail à partir d'un tag

getWorkInfosByTagLiveData

Enfin, si vous le souhaitez, vous pouvez ajouter un tag à n'importe quelle WorkRequest avec une chaîne. Vous pouvez utiliser le même tag pour plusieurs WorkRequest afin de les associer. Cela renvoie LiveData
>
pour n'importe quel tag.

Vous allez ajouter le tag SaveImageToFileWorker WorkRequest pour le récupérer à l'aide de getWorkInfosByTag. Vous allez utiliser un tag pour étiqueter votre travail au lieu d'utiliser l'ID WorkManager. Ainsi, si l'utilisateur floute plusieurs images, toutes les WorkRequest d'enregistrement d'image seront associées au même tag, mais pas au même ID. Vous pouvez également choisir le tag.

Vous ne devez pas utiliser getWorkInfosForUniqueWork, car cela renverrait WorkInfo pour toutes les WorkRequest de floutage et les WorkRequest de nettoyage. Une logique supplémentaire serait nécessaire pour trouver la WorkRequest d'enregistrement d'image.

Étape 1 : Ajoutez un tag à votre travail

Dans applyBlur, lorsque vous créez SaveImageToFileWorker, ajoutez un tag à votre travail à l'aide de la constante String TAG_OUTPUT :

BlurViewModel.kt

val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .addTag(TAG_OUTPUT) // <-- ADD THIS
        .build()

Étape 2 : Obtenez WorkInfo

Maintenant que vous avez ajouté un tag à votre travail, vous pouvez obtenir WorkInfo :

  1. Dans BlurViewModel, déclarez une nouvelle variable de classe appelée outputWorkInfos, qui est un LiveData<List<WorkInfo>>.
  2. Dans BlurViewModel, ajoutez un bloc init pour obtenir WorkInfo à l'aide de WorkManager.getWorkInfosByTagLiveData.

Le code dont vous avez besoin est donné ci-dessous :

BlurViewModel.kt

// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>

// Modify the existing init block in the BlurViewModel class to this:
init {
    imageUri = getImageUri(application.applicationContext)
    // This transformation makes sure that whenever the current work Id changes the WorkInfo
    // the UI is listening to changes
    outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
}

Étape 3 : Affichez WorkInfo

Maintenant que vous disposez de LiveData pour WorkInfo, vous pouvez les observer dans BlurActivity. Dans l'observateur :

  1. Vérifiez que la liste de WorkInfo n'est pas nulle et qu'elle contient des objets WorkInfo. Si ce n'est pas le cas, cela signifie que l'utilisateur n'a pas encore cliqué sur le bouton Go (Appliquer), vous devez donc la renvoyer.
  2. Obtenez les premières WorkInfo de la liste. Il n'y aura toujours qu'une seule WorkInfo taguée avec TAG_OUTPUT, car la chaîne de travail est unique.
  3. Vérifiez si l'état du travail est terminé en utilisant workInfo.state.isFinished.
  4. Si ce n'est pas le cas, appelez la méthode showWorkInProgress(), qui masque le bouton Go (Appliquer), et affiche le bouton Cancel Work (Annuler le travail) et la barre de progression.
  5. S'il est terminé, appelez la méthode showWorkFinished(), qui masque le bouton Cancel Work (Annuler le travail) et la barre de progression, puis affiche le bouton Go (Appliquer).

Voici le code :

Remarque : Importez androidx.lifecycle.Observer si nécessaire.

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Observe work status, added in onCreate()
    viewModel.outputWorkInfos.observe(this, workInfosObserver())
}

// Define the observer function
private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()
        } else {
            showWorkInProgress()
        }
    }
}

Étape 4 : Exécutez votre application

Exécutez votre application. Elle devrait se compiler et s'exécuter, et afficher à présent une barre de progression ainsi que le bouton d'annulation lorsqu'elle s'exécute :

7b70288f69050f0b.png

9. Afficher le résultat final

Chaque WorkInfo possède également une méthode getOutputData qui vous permet d'obtenir l'objet Data de sortie avec l'image finale enregistrée. Dans Kotlin, vous pouvez accéder à cette méthode à l'aide d'une variable générée par la langue : outputData. Affichons à présent un bouton See File (Consulter le fichier) lorsqu'une image floue est prête à être affichée.

Étape 1 : Créez le bouton "See File" (Consulter le fichier)

La mise en page activity_blur.xml contient déjà un bouton masqué. Il se trouve dans BlurActivity et s'appelle outputButton.

Dans BlurActivity, à l'intérieur de onCreate(), configurez l'écouteur de clics pour ce bouton. Il devrait obtenir l'URI, puis ouvrir une activité pour afficher cet URI. Vous pouvez utiliser le code ci-dessous :

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   // Setup view output image file button
   binding.seeFileButton.setOnClickListener {
       viewModel.outputUri?.let { currentUri ->
           val actionView = Intent(Intent.ACTION_VIEW, currentUri)
           actionView.resolveActivity(packageManager)?.run {
               startActivity(actionView)
           }
       }
   }
}

Étape 2 : Définissez l'URI et affichez le bouton

Il vous reste quelques réglages finaux à appliquer à l'observateur WorkInfo pour que cela fonctionne :

  1. Si WorkInfo est terminé, obtenez les données de sortie à l'aide de workInfo.outputData.
  2. Obtenez ensuite l'URI de sortie. Gardez à l'esprit qu'il est stocké avec la clé Constants.KEY_IMAGE_URI.
  3. Si l'URI n'est pas vide, il est enregistré correctement. Affichez outputButton et appelez setOutputUri sur le modèle de vue avec l'URI.

BlurActivity.kt

private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()

            // Normally this processing, which is not directly related to drawing views on
            // screen would be in the ViewModel. For simplicity we are keeping it here.
            val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)

            // If there is an output file show "See File" button
            if (!outputImageUri.isNullOrEmpty()) {
                viewModel.setOutputUri(outputImageUri)
                binding.seeFileButton.visibility = View.VISIBLE
            }
        } else {
            showWorkInProgress()
        }
    }
}

Étape 3 : Exécutez le code

Exécutez votre code. Vous devriez voir votre nouveau bouton cliquable See File (Consulter le fichier), qui vous redirige vers le fichier généré :

5366222d0b4fb705.png

cd1ecc8b4ca86748.png

10. Annuler le travail

bc1dc9414fe2326e.png

Vous avez ajouté le bouton Cancel Work (Annuler le travail). Ajoutons à présent le code pour qu'il effectue une action. Avec WorkManager, vous pouvez annuler votre travail par ID, par tag et par nom de chaîne unique.

Dans le cas présent, vous devez annuler le travail par nom de chaîne unique, car vous devez annuler tous les travaux de la chaîne, pas seulement d'une étape spécifique.

Étape 1 : Annulez le travail par son nom

Dans BlurViewModel, ajoutez une méthode appelée cancelWork() pour annuler le travail unique. Dans la fonction, appelez cancelUniqueWork sur workManager, et transmettez le tag IMAGE_MANIPULATION_WORK_NAME.

BlurViewModel.kt

internal fun cancelWork() {
    workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}

Étape 2 : Appelez la méthode d'annulation

Ensuite, associez le bouton cancelButton à l'opération cancelWork :

BlurActivity.kt

// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }

Étape 3 : Exécutez et annulez votre travail

Exécutez votre application. Elle devrait se compiler correctement. Commencez à flouter une image, puis cliquez sur le bouton d'annulation. Toute la chaîne est annulée.

dcb4ccfd261957b1.png

Notez que le bouton "GO" (Appliquer) n'est désormais affiché qu'une fois le travail annulé, car WorkState n'est plus à l'état FINISHED (Terminé).

11. Contraintes liées aux travaux

Enfin et surtout, WorkManager est compatible avec Constraints. Pour Blur-O-Matic, vous allez utiliser la contrainte suivante : l'appareil doit être en train de se charger. Cela signifie que votre requête de travail ne sera exécutée que si l'appareil est en charge.

Étape 1 : Créez et ajoutez une contrainte de charge

Pour créer un objet Constraints, utilisez un Constraints.Builder. Définissez ensuite les contraintes souhaitées et ajoutez-les à la WorkRequest à l'aide de la méthode setRequiresCharging(), comme indiqué ci-dessous :

Importez androidx.work.Constraints, si nécessaire.

BlurViewModel.kt

// Put this inside the applyBlur() function, above the save work request.
// Create charging constraint
val constraints = Constraints.Builder()
        .setRequiresCharging(true)
        .build()

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .setConstraints(constraints)
        .addTag(TAG_OUTPUT)
        .build()
continuation = continuation.then(save)

// Actually start the work
continuation.enqueue()

Étape 2 : Effectuez un test avec l'émulateur ou l'appareil

Vous pouvez à présent exécuter Blur-O-Matic. Si vous utilisez un appareil, vous pouvez le retirer ou le connecter. Sur un émulateur, vous pouvez modifier l'état de charge dans la fenêtre de commandes avancées :

406ce044ca07169f.png

Lorsque l'appareil ne se charge pas, il doit suspendre SaveImageToFileWorker, et ne l'exécuter qu'après que vous avez branché l'appareil.

302da5ec986ae769.png

12. Félicitations

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 travaux et de WorkRequest
  • Attribution d'un nom à des chaînes WorkRequest uniques
  • Ajout de tags à WorkRequest
  • Affichage de WorkInfo dans l'interface utilisateur
  • Annulation de WorkRequest
  • Ajout de contraintes à une WorkRequest

Beau travail ! Pour voir l'état final du code et toutes les modifications, consultez la page :

Télécharger le code final

Ou, si vous préférez, vous pouvez cloner l'atelier de programmation de WorkManager à partir de GitHub :

$ git clone https://github.com/googlecodelabs/android-workmanager

WorkManager est compatible avec bien d'autres fonctionnalités que nous n'avons pu traiter dans cet atelier de programmation, comme les travaux répétitifs, 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 de WorkManager ou passez à l'atelier de programmation "Fonctionnalités avancées de WorkManager".