Trabajo en segundo plano con WorkManager (Kotlin)

1. Introducción

En Android, hay muchas opciones para realizar trabajos diferibles en segundo plano. Este codelab trata sobre WorkManager, una biblioteca con retrocompatibilidad, flexible y simple para realizar trabajos diferibles en segundo plano. WorkManager es el programador de tareas recomendado de Android para realizar trabajos diferibles, lo que garantiza su ejecución.

¿Qué es WorkManager?

WorkManager es parte de Android Jetpack y un componente de la arquitectura para trabajos en segundo plano que requieren una ejecución tanto oportunista como garantizada. La ejecución oportunista implica que WorkManager realizará el trabajo en segundo plano tan pronto como sea posible. La ejecución garantizada implica que WorkManager se encargará de la lógica a los efectos de iniciar tu trabajo en diferentes situaciones, incluso si sales de la app.

WorkManager es una biblioteca extremadamente flexible que cuenta con muchos beneficios adicionales. Por ejemplo:

  • Es compatible con tareas asíncronas únicas y periódicas.
  • Admite restricciones, como condiciones de red, espacio de almacenamiento y estado de carga.
  • Encadena solicitudes de trabajo complejas, incluido el trabajo de ejecución en paralelo.
  • Usa el resultado de una solicitud de trabajo como entrada para la siguiente.
  • Controla la compatibilidad con el nivel de API 14 (consulta la nota).
  • Trabaja con o sin los Servicios de Google Play.
  • Sigue las prácticas recomendadas sobre el estado del sistema.
  • Ofrece compatibilidad con LiveData a fin de mostrar fácilmente el estado de la solicitud de trabajo en la IU.

Cuándo usar WorkManager

La biblioteca de WorkManager es una buena opción para las tareas que resultan útiles de completar, incluso si el usuario sale de una pantalla en particular o de tu app.

Algunos ejemplos de tareas que muestran un buen uso de WorkManager:

  • Subir registros
  • Aplicar filtros a imágenes y guardar la imagen
  • Sincronizar datos locales con la red de forma periódica

WorkManager ofrece una ejecución garantizada, y no todas las tareas lo necesitan. Por consiguiente, su función no es la de ejecutar todas las tareas del subproceso principal. Si quieres obtener más información sobre el uso de WorkManager, consulta la Guía para el procesamiento en segundo plano.

Qué compilarás

Hoy en día, los smartphones son muy buenos para tomar fotos. Atrás quedaron los días en que un fotógrafo tomaba fotos desenfocadas de objetos misteriosos.

En este codelab, trabajarás en Blur-O-Matic, una app que desenfoca imágenes y fotos, y guarda el resultado en un archivo. ¿Eso era el monstruo del Lago Ness o un submarino de juguete? Con Blur-O-Matic, nadie lo sabrá jamás.

Imagen de la app en estado completo, con una imagen de marcador de posición de una magdalena, 3 opciones para aplicar desenfoque a la imagen y 2 botones: uno para desenfocar la imagen y otro para verla.

Imagen borrosa, tal como se ve después de hacer clic en “See File”.

Qué aprenderás

  • Cómo agregar WorkManager a tu proyecto
  • Cómo programar una tarea simple
  • Parámetros de entrada y salida
  • Trabajos en cadena
  • Trabajo único
  • Cómo mostrar el estado de trabajo en la IU
  • Cómo cancelar trabajos
  • Restricciones de trabajos

Requisitos

2. Cómo prepararte

Paso 1: Descarga el código

Haz clic en el siguiente vínculo a fin de descargar todo el código de este codelab:

Si lo prefieres, también puedes clonar el codelab de WorkManager desde GitHub:

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

Paso 2: Ejecuta la app

Ejecuta la app. Deberías ver la siguiente pantalla:

9e4707e0fbdd93c7.png

La pantalla debe tener botones de selección con los que puedes elegir cuánto desenfocarás tu imagen. Si presionas el botón Go, la imagen se desenfocará y se guardará.

A partir de este momento, la app dejará de desenfocar la imagen.

El código inicial contiene lo siguiente:

  • WorkerUtils: Esta clase contiene el código para desenfocar una imagen y algunos métodos útiles que usarás más tarde para mostrar Notifications, guardar un mapa de bits para archivar y ralentizar la app.
  • BlurActivity:* Esta es la actividad que muestra la imagen e incluye botones de selección para elegir el grado de desenfoque.
  • BlurViewModel:* Este modelo de vista almacena todos los datos necesarios a fin de mostrar la BlurActivity. También será la clase en la que inicies el trabajo en segundo plano con WorkManager.
  • Constants: Esta es una clase estática con algunas constantes que usarás durante el codelab.
  • res/activity_blur.xml: Los archivos de diseño para BlurActivity.

***** Estos son los únicos archivos en los que escribirás código.

3. Agrega WorkManager a tu app

WorkManager requiere la dependencia de Gradle que se indica a continuación. Estas ya se incluyeron en los archivos de compilación:

app/build.gradle

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

Deberás obtener la versión estable más reciente de work-runtime-ktx aquí y colocar la versión correcta. En este momento, la versión más reciente es:

build.gradle

versions.work = "2.7.1"

Si actualizas tu versión a una más reciente, asegúrate de usar la opción Sincronizar ahora a fin de sincronizar tu proyecto con los archivos de Gradle modificados.

4. Crea tu primera WorkRequest

En este paso, tomarás una imagen de la carpeta res/drawable llamada android_cupcake.png y ejecutarás algunas funciones sobre ella en segundo plano. Estas funciones desenfocarán la imagen y la guardarán en un archivo temporal.

Aspectos básicos de WorkManager

Existen algunas clases de WorkManager que debes conocer:

  • Worker: Aquí es donde colocas el código del trabajo real que deseas realizar en segundo plano. Extenderás esta clase y anularás el método doWork().
  • WorkRequest: Esta clase representa una solicitud para realizar algunos trabajos. Como parte de la creación de tu WorkRequest, pasarás el Worker. Cuando hagas la WorkRequest, también podrás especificar elementos como Constraints sobre el momento en que se debe ejecutar el Worker.
  • WorkManager: Esta clase programa tu WorkRequest y la ejecuta. Programa WorkRequests de manera que se distribuya la carga sobre los recursos del sistema, respetando las restricciones que hayas especificado.

En tu caso, definirás un nuevo BlurWorker que contendrá el código para desenfocar una imagen. Cuando se haga clic en el botón Go, se creará una WorkRequest y, luego, WorkManager lo pondrá en cola.

Paso 1: Crea el BlurWorker

En el paquete workers, crea una nueva clase de Kotlin llamada BlurWorker.

Paso 2: Agrega un constructor

Agrega una dependencia a Worker para la clase BlurWorker:

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

Paso 3: Anula e implementa doWork()

Tu Worker desenfocará la imagen de la magdalena que se muestra.

Para ver mejor cuándo se ejecuta el trabajo, usarás la clase makeStatusNotification() de WorkerUtil. Este método te permitirá ver fácilmente un banner de notificación en la parte superior de la pantalla.

Anula el método doWork() y, luego, implementa lo siguiente. Puedes consultar el código completo al final de la sección:

  1. Obtén un Context llamando a la propiedad applicationContext. Asígnalo a un nuevo val llamado appContext. Lo necesitarás para las diferentes manipulaciones de mapas de bits que harás.
  2. Muestra una notificación de estado con la función makeStatusNotification para notificar al usuario sobre el desenfoque de la imagen.
  3. Crea un Bitmap a partir de la imagen de la magdalena:
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.android_cupcake)
  1. Obtén una versión desenfocada del mapa de bits llamando al método blurBitmap desde WorkerUtils.
  2. Escribe ese mapa de bits en un archivo temporal llamando al método writeBitmapToFile desde WorkerUtils. Asegúrate de guardar el URI que se muestra en una variable local.
  3. Realiza una notificación en la que se muestre el URI llamando al método makeStatusNotification desde WorkerUtils.
  4. Muestra Result.success().
  5. Une el código de los pasos 3 a 6 en una sentencia try/catch. Captura un elemento Throwable genérico.
  6. En la sentencia catch, imprime un mensaje de error con la instrucción de registro Log.e(TAG, "Error applying blur").
  7. Luego, en la sentencia catch, muestra Result.failure().

A continuación, se incluye el código completo correspondiente a este paso.

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

Paso 4: Obtén WorkManager en el ViewModel

Crea una variable de clase para una instancia WorkManager en tu ViewModel:

BlurViewModel.kt

private val workManager = WorkManager.getInstance(application)

Paso 5: Pon en cola la WorkRequest en WorkManager

Es hora de crear una WorkRequest y pedirle a WorkManager que la ejecute. Existen dos tipos de WorkRequest:

  • OneTimeWorkRequest: Es una WorkRequest que solo se ejecutará una vez.
  • PeriodicWorkRequest: Es una WorkRequest que se repetirá en forma cíclica.

Solo queremos que la imagen se desenfoque una vez cuando se haga clic en el botón Go. Se llamará al método applyBlur cuando se haga clic en el botón Go, por lo que deberás crear una OneTimeWorkRequest desde BlurWorker. Luego, usa tu instancia de WorkManager para poner en cola tu WorkRequest.

Agrega la siguiente línea de código al método BlurViewModel's applyBlur():

BlurViewModel.kt

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

Paso 6: Ejecuta el código

Ejecuta tu código. Debería compilarse, y tú deberías ver la notificación cuando presiones el botón Go. Ten en cuenta que, para ver un resultado más desenfocado, debes seleccionar las opciones “More blurred” o “The most blurred”.

ed497b57e1f527be.png

Para confirmar que la imagen se desenfocó correctamente, puedes abrir el Explorador de archivos de dispositivos en Android Studio:

cf10a1af6e84f5ff.png

Luego, navega a data > data > com.example.background > files > blur_filter_outputs> <URI> y confirma que la magdalena sí esté desenfocada:

e1f61035d680ba03.png

5. Agrega parámetros de entrada y salida

Desenfocar el elemento de imagen en el directorio de recursos está muy bien, pero para que Blur-O-Matic en verdad sea una app revolucionaria de edición de imágenes, debes permitir que el usuario desenfoque la imagen que ve en la pantalla y que puedan ver el resultado desenfocado.

Para ello, proporcionaremos el URI de la imagen de la magdalena que se muestra como entrada para nuestro WorkRequest mostrado y, luego, usaremos la salida de la WorkRequest para mostrar imagen borrosa final.

Paso 1: Crea un objeto de entrada de datos

La entrada y el resultado se pasan en un sentido y otro por medio de objetos Data. Los objetos Data son contenedores livianos para pares clave-valor. Tienen el propósito de almacenar una pequeña cantidad de datos que podrían pasar desde WorkRequests y hacia ellas.

Pasarás el URI de la imagen del usuario a un paquete. Ese URI se almacenará en una variable llamada imageUri.

En BlurViewModel, crea un método privado llamado createInputDataForUri. Con este método, harás lo siguiente:

  1. Crea un objeto Data.Builder. Importa androidx.work.Data cuando se te solicite hacerlo.
  2. Si imageUri es un URI no nulo, lo agregarás al objeto Data mediante el método putString. Este método toma una clave y un valor. Puedes usar la constante KEY_IMAGE_URI de string de la clase Constants.
  3. Llama build() en el objeto Data.Builder a fin de crear tu objeto Data y mostrarlo.

A continuación, se muestra el método createInputDataForUri completo:

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

Paso 2: Pasa el objeto de datos a WorkRequest

Cambiarás el método applyBlur en BlurViewModel de modo que realice lo siguiente:

  1. Cree una OneTimeWorkRequestBuilder nueva.
  2. Llame a setInputData y pase el resultado de createInputDataForUri
  3. Compile el OneTimeWorkRequest.
  4. Ponga en cola la solicitud de trabajo mediante la solicitud WorkManager a fin de que el trabajo se programe para ejecutarse.

A continuación, se muestra el método applyBlur completo:

BlurViewModel.kt

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

    workManager.enqueue(blurRequest)
}

Paso 3: Actualiza el método doWork() de BlurWorker para obtener la entrada

Actualicemos el método doWork() de BlurWorker a fin de obtener el URI que pasamos del objeto Data:

BlurWorker.kt

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

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

Paso 4: Difumina el URI dado

Con el URI, desenfocaremos la imagen de la magdalena en la pantalla.

  1. Quita el código anterior que tenía el recurso de imagen.

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

  1. Verifica que no esté vacío el resourceUri obtenido de Data que se pasó.
  2. Asigna la variable picture para que sea la imagen que se pasó de la siguiente manera:

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

Paso 5: URI temporal resultante

Ahora que terminaste con este trabajador, puedes mostrar el URI resultante en Result.success(). Proporciona el URI de salida como datos de salida para facilitar el acceso a esta imagen temporal a otros trabajadores a fin de que realicen otras operaciones. Esto será útil en el siguiente capítulo, en el que crearás una cadena de trabajadores. Para ello, sigue estos pasos:

  1. Crea un nuevo objeto Data, tal como lo hiciste con la entrada, y almacena outputUri como una String. Usa la misma clave, KEY_IMAGE_URI.
  2. Muestra esto a WorkManager mediante el método Result.success(Data outputData).

BlurWorker.kt

Modifica la línea Result.success() en doWork() de la siguiente manera:

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

Result.success(outputData)

Paso 6: Ejecuta tu app

En este punto, debes ejecutar tu app. Debería compilarse y tener el mismo comportamiento en el que puedes ver la imagen difuminada a través del explorador de archivos de dispositivos, pero aún no en la pantalla.

Para buscar otra imagen desenfocada, puedes abrir el Explorador de archivos de dispositivos en Android Studio y navegar hasta data/data/com.example.background/files/blur_filter_outputs/<URI> tal como lo hiciste en el paso anterior.

Ten en cuenta que quizás debas realizar una sincronización para ver tus imágenes:

7e717ffd6b3d9d52.png

¡Muy bien! Desenfocaste una imagen de entrada usando WorkManager.

6. Encadena tu trabajo

De momento, estás realizando una sola tarea: desenfocar la imagen. Este es un excelente primer paso, pero carece de algunas funciones principales:

  • No limpia los archivos temporales.
  • En realidad, no guarda la imagen en un archivo permanente.
  • Siempre desenfoca la imagen de la misma manera.

Para agregar estas funciones, usaremos una cadena de trabajos de WorkManager.

WorkManager te permite crear WorkerRequest independientes que se ejecutan en orden, o bien en paralelo. En este paso, crearás una cadena de trabajo que tiene el siguiente aspecto:

54832b34e9c9884a.png

Las WorkRequest se representan como cuadros.

Otra característica muy interesante del encadenamiento es que el resultado de una WorkRequest se convierte en la entrada de la próxima WorkRequest de la cadena. La entrada y el resultado que se pasan entre cada WorkRequest se muestran como texto azul.

Paso 1: Crea trabajadores de limpieza y almacenamiento

Primero, deberás definir todas las clases Worker que necesites. Ya tienes un Worker para desenfocar una imagen, pero también necesitarás un Worker que limpie los archivos temporales y un Worker que guarde la imagen de forma permanente.

Crea dos clases nuevas en el paquete workers que extiende Worker.

El primero debe llamarse CleanupWorker y, el segundo, SaveImageToFileWorker.

Paso 2: Haz que extienda Worker

Extiende la clase CleanupWorker de la clase Worker. Agrega los parámetros de constructor requeridos.

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

Paso 3: Anula e implementa doWork() para CleanupWorker

CleanupWorker no necesita tomar ninguna entrada ni pasar ningún resultado. Siempre borrará los archivos temporales que existan. Como la manipulación de archivos está fuera del alcance de este codelab, puedes copiar el código del CleanupWorker a continuación:

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

Paso 4: Anula e implementa doWork() para SaveImageToFileWorker

SaveImageToFileWorker admitirá entradas y resultados. La entrada es una String del URI de la imagen difuminada temporalmente y almacenada con la clave KEY_IMAGE_URI. El resultado también será una String, el URI de la imagen difuminada guardada que se almacenó con la clave KEY_IMAGE_URI.

4fc29ac70fbecf85.png

Dado que este no es un codelab sobre la manipulación de archivos, a continuación se proporciona el código. Observa cómo se recuperan los valores resourceUri y output con la clave KEY_IMAGE_URI. Esto es muy similar al código que escribiste en el paso anterior para la entrada y el resultado (utiliza las mismas claves).

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

Paso 5: Modifica la notificación de BlurWorker

Ahora que tenemos una cadena de Worker que se encarga de guardar las imágenes en la carpeta correcta, podemos ralentizar el trabajo mediante el método sleep() definido en la clase WorkerUtils para que sea más fácil ver cada inicio de WorkRequest, incluso en dispositivos emulados. La versión final de BlurWorker será la siguiente:

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

Paso 6: Crea una cadena de WorkRequest

Debes modificar el método applyBlur de BlurViewModel a fin de ejecutar una cadena de WorkRequest en lugar de ejecutar solo una. Actualmente, el código tiene el siguiente aspecto:

BlurViewModel.kt

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

workManager.enqueue(blurRequest)

En lugar de llamar a workManager.enqueue(), llama a workManager.beginWith(). Esto mostrará un WorkContinuation, que define una cadena de WorkRequest. Puedes agregar a esta cadena de solicitudes de trabajo llamando al método then(), por ejemplo, si tienes tres objetos WorkRequest, workA, workB y workC, podrás hacer lo siguiente:

// 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

Esto produciría y ejecutaría la siguiente cadena de WorkRequests:

bf3b82eb9fd22349.png

Crea una cadena de WorkRequest de CleanupWorker, WorkRequest de BlurImage y WorkRequest de SaveImageToFile en applyBlur. Pasa la entrada a la WorkRequest de BlurImage.

El código correspondiente se muestra a continuación:

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

Esto debería compilar y ejecutar. Ahora, deberías poder presionar el botón Go y ver las notificaciones cuando se ejecuten los diferentes trabajadores. Aún podrás ver la imagen difuminada en el explorador de archivos de dispositivos y, en un paso siguiente, agregarás un botón adicional para que los usuarios puedan verla en el dispositivo.

En las siguientes capturas de pantalla, notarás que los mensajes de notificación muestran qué trabajador se está ejecutando actualmente.

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

Paso 7: Repite el BlurWorker

Es hora de agregar la capacidad de desenfocar una imagen varias veces. Toma el parámetro blurLevel que pasaste a applyBlur y agrega esa cantidad de operaciones WorkRequest de desenfoque a la cadena. Solo la primera WorkRequest necesitará contar con la entrada de URI.

Pruébalo y, luego, compáralo con el siguiente código:

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

Para ver las imágenes difuminadas, abre el explorador de archivos de dispositivos. Ten en cuenta que la carpeta de salida contiene varias imágenes desenfocadas, imágenes que se encuentran en las etapas intermedias de esta acción y la imagen final que muestra la imagen desenfocada en función de la cantidad de desenfoque que seleccionaste.

¡Buen “trabajo”! Ahora podrás desenfocar la imagen tanto como quieras. ¡Qué misterioso!

7. Garantiza el trabajo único

Ahora que usaste las cadenas, es hora de abordar otra poderosa función de WorkManager: las cadenas de trabajo único.

A veces, querrás que solo una cadena de trabajo se ejecute a la vez. Por ejemplo, tal vez tengas una cadena de trabajo que sincroniza tus datos locales con el servidor. Sería bueno permitir que la primera sincronización de datos termine antes de comenzar una nueva. Para hacerlo, deberás usar beginUniqueWork en lugar de beginWith y proporcionarle un nombre de String único. Esto nombrará la cadena completa de solicitudes de trabajo a fin de que puedas hacer consultas y búsquedas en todas ellas.

Asegúrate de que la cadena de trabajo que desenfocará tu archivo sea única por medio de beginUniqueWork. Pasa IMAGE_MANIPULATION_WORK_NAME como la clave. También deberás pasar una ExistingWorkPolicy. Tus opciones son REPLACE, KEEP o APPEND.

Deberás usar REPLACE porque, si el usuario decide desenfocar otra imagen antes de que se termine la actual, querremos detener la tarea actual y comenzar a desenfocar la imagen nueva.

El código para iniciar la continuación del trabajo único es el siguiente:

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 ahora solo desenfocará una imagen por vez.

8. Etiqueta y muestra el estado del trabajo

En esta sección, se usará LiveData de manera considerable, por lo que, a fin de que aproveches por completo lo que sigue, deberías estar familiarizado con LiveData. LiveData es un contenedor de datos observable optimizado para ciclos de vida.

Puedes consultar la documentación o el Codelab de componentes optimizados para ciclos de vida de Android si es la primera vez que trabajas con LiveData o clases observables.

El siguiente cambio importante que harás será cambiar lo que se muestra en la app a medida que se ejecuta el trabajo.

Puedes obtener el estado de cualquier WorkRequest si obtienes un LiveData que contiene un objeto WorkInfo. WorkInfo es un objeto que contiene detalles sobre el estado actual de una WorkRequest, incluido lo siguiente:

En la siguiente tabla, se muestran tres formas diferentes de obtener objetos LiveData<WorkInfo> o LiveData<List<WorkInfo>>, así como lo que hace cada uno.

Tipo

Método de WorkManager

Descripción

Obtener trabajo con un ID

getWorkInfoByIdLiveData

Cada WorkRequest tiene un ID único generado por WorkManager. Puedes usarlo a fin de obtener un único LiveData
para esa WorkRequest exacta.

Obtener trabajo con un nombre de cadena único

getWorkInfosForUniqueWorkLiveData

Como acabas de ver, las WorkRequest pueden ser parte de una cadena única. Esto muestra el LiveData
>
de todo el trabajo en una sola cadena única de WorkRequests.

Obtener trabajo con una etiqueta

getWorkInfosByTagLiveData

Por último, también puedes etiquetar cualquier WorkRequest por medio de una String. Puedes etiquetar varias WorkRequest con la misma etiqueta a fin de asociarlas. Esto muestra el LiveData
>
de cualquier etiqueta individual.

Etiqueta la WorkRequest de SaveImageToFileWorker de modo que puedas obtenerla usando getWorkInfosByTag. A los efectos de etiquetar tu trabajo, usa una etiqueta en lugar del ID de WorkManager, ya que si el usuario desenfoca varias imágenes, todas las WorkRequest para guardar imágenes tendrán la misma etiqueta, pero no el mismo ID. También puedes elegir la etiqueta.

No usarás getWorkInfosForUniqueWork, ya que eso mostraría la WorkInfo para todas las WorkRequest de desenfoque y de limpieza WorkRequest. Se necesitaría introducir lógica adicional a fin de encontrar la WorkRequest correspondiente al guardado de la imagen.

Paso 1: Etiqueta tu trabajo

En applyBlur, cuando crees el SaveImageToFileWorker, etiqueta tu trabajo con la constante de String TAG_OUTPUT :

BlurViewModel.kt

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

Paso 2: Obtén la WorkInfo

Ahora que etiquetaste el trabajo, podrás obtener la WorkInfo:

  1. En BlurViewModel, declara una nueva variable de clase llamada outputWorkInfos, que es un elemento LiveData<List<WorkInfo>>.
  2. En BlurViewModel, agrega un bloque init a fin de obtener la WorkInfo con WorkManager.getWorkInfosByTagLiveData.

El código que necesitas es el siguiente:

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

Paso 3: Muestra la WorkInfo

Ahora que tienes un LiveData para tu WorkInfo, puedes observarla en BlurActivity. En el observador, haz lo siguiente:

  1. Verifica si la lista de WorkInfo no es nula y si tiene objetos WorkInfo en ella. De no ser así, entonces aún no se hizo clic en el botón Go. En ese caso, vuelve atrás.
  2. Obtén las primeras WorkInfo de la lista; solo habrá una WorkInfo etiquetada con TAG_OUTPUT, ya que hicimos que la cadena de trabajo fuera única.
  3. Comprueba si finalizó el estado del trabajo mediante workInfo.state.isFinished.
  4. Si no está terminado, llama a showWorkInProgress(), que oculta el botón Go y muestra el botón Cancel Work y la barra de progreso.
  5. Si se completó el proceso, llama a showWorkFinished(), que ocultará el botón Cancel Work y la barra de progreso, y mostrará el botón Go.

Este es el código:

Nota: Importa androidx.lifecycle.Observer cuando se te solicite hacerlo.

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

Paso 4: Ejecuta tu app

Ejecuta tu app. Debería compilarse y ejecutarse, y ahora mostrará una barra de progreso cuando esté funcionando, así como el botón Cancelar:

7b70288f69050f0b.png

9. Muestra el resultado final

Cada WorkInfo también tiene un método getOutputData que te permitirá obtener el objeto Data resultante junto con la imagen final guardada. En Kotlin, puedes acceder a este método usando una variable que el lenguaje genera para ti: outputData. Mostremos un botón que diga See File cuando haya una imagen desenfocada lista para mostrarse.

Paso 1: Crea el botón See File

Ya hay un botón en el diseño de activity_blur.xml que está oculto. Se encuentra en BlurActivity y se llama outputButton.

En BlurActivity, dentro de onCreate(), configura el objeto de escucha de clics para ese botón. Deberías obtener el URI y, luego, abrir una actividad a fin de verlo. Puedes usar el siguiente código:

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

Paso 2: Establece el URI y muestra el botón

Para hacer esto, deberás aplicar algunos ajustes finales al observador de WorkInfo:

  1. Si WorkInfo finalizó, obtén los datos resultantes por medio de workInfo.outputData.
  2. Luego, obtén el URI resultante. Recuerda que se almacena con la clave Constants.KEY_IMAGE_URI.
  3. Luego, si el URI no está vacío, se guardará correctamente. Muestra el outputButton y llama a setOutputUri en el modelo de vista con el 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()
        }
    }
}

Paso 3: Ejecuta el código

Ejecuta tu código. Deberías ver el nuevo botón See File en el que se puede hacer clic, que te llevará al archivo resultante:

5366222d0b4fb705.png

cd1ecc8b4ca86748.png

10. Cancela el trabajo

bc1dc9414fe2326e.png

Agregaste este botón Cancel Work. Ahora agreguemos el código para que haga algo. Con WorkManager, puedes cancelar trabajos usando el ID, por etiqueta y por nombre de cadena único.

En este caso, querrás cancelar el trabajo por nombre de cadena único, ya que quieres cancelar todo el trabajo de la cadena, no solo un paso en particular.

Paso 1: Cancela el trabajo por nombre

En BlurViewModel, agrega un método nuevo llamado cancelWork() para cancelar el trabajo único. Dentro de la función, llama a cancelUniqueWork en el workManager y pasa la etiqueta IMAGE_MANIPULATION_WORK_NAME.

BlurViewModel.kt

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

Paso 2: Llama al método de cancelación

Luego, conecta el botón cancelButton para que llame a cancelWork:

BlurActivity.kt

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

Paso 3: Ejecuta y cancela tu trabajo

Ejecuta la app. Debería compilarse bien. Empieza a desenfocar una imagen y, luego, haz clic en el botón Cancelar. Se cancelará toda la cadena.

dcb4ccfd261957b1.png

Ten en cuenta que solo se muestra el botón GO una vez que se cancela el trabajo porque WorkState ya no está en estado FINISHED.

11. Restricciones de trabajos

Por último, WorkManager admite Constraints. En el caso de Blur-O-Matic, usarás la restricción que establece que el dispositivo deberá estar cargándose. Esto significa que tu solicitud de trabajo solo se ejecutará si el dispositivo se está cargando.

Paso 1: Crea y agrega la restricción de carga

Para crear un objeto Constraints, deberás usar un Constraints.Builder. Luego, deberás configurar las restricciones que desees y agregarlas a la WorkRequest con el método setRequiresCharging() como se muestra a continuación:

Importa androidx.work.Constraints cuando se te solicite hacerlo.

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

Paso 2: Realiza las pruebas con el emulador o el dispositivo

Ya puedes ejecutar Blur-O-Matic. Si estás usando un dispositivo, puedes quitarlo o conectarlo. En un emulador, puedes cambiar el estado de carga en la ventana Extended controls:

406ce044ca07169f.png

Cuando el dispositivo esté desconectado, deberá suspender SaveImageToFileWorker, y ejecutarlo solo después de que lo conectes.

302da5ec986ae769.png

12. Felicitaciones

Felicitaciones. Terminaste la app de Blur-O-Matic y, en el proceso, aprendiste lo siguiente:

  • Cómo agregar WorkManager a tu Proyecto
  • Cómo programar un OneTimeWorkRequest
  • Parámetros de entrada y salida
  • Cómo encadenar el trabajo con WorkRequest
  • Cómo asignar nombres a cadenas de WorkRequest únicas
  • Cómo etiquetar WorkRequest
  • Cómo mostrar WorkInfo en la IU
  • Cómo cancelar WorkRequest
  • Cómo agregar restricciones a una WorkRequest

¡Buen "trabajo"! Para ver el estado final del código y todos los cambios, consulta lo siguiente:

También puedes clonar el codelab de WorkManager desde GitHub:

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

WorkManager admite mucho más de lo que podríamos abarcar en este codelab, incluido el trabajo repetitivo, una biblioteca de compatibilidad de pruebas, solicitudes de trabajo paralelas y combinaciones de entrada. Para obtener más información, consulta la documentación de WorkManager o continúa con el codelab avanzado WorkManager.