Trabajo en segundo plano con WorkManager (Kotlin)

En Android, hay muchas opciones para realizar trabajos diferibles en segundo plano. Este codelab trata sobre WorkManager, una biblioteca con retrocompatibilidad, flexible y simple a fin de realizar trabajos diferibles en segundo plano. WorkManager es el programador de tareas recomendado de Android para realizar trabajos diferibles, 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.
  • Utiliza el resultado de una solicitud de trabajo como entrada para la siguiente.
  • Controla la compatibilidad con el nivel de API 14 (consulta la nota).
  • Funciona con o sin los Servicios de Google Play.
  • Sigue las recomendaciones para proteger el 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 tomara con seguridad una foto desenfocada de algo misterioso.

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

Foto de lubina estriada híbrida, tomada por Peggy Greb, Servicio de Investigación Agrícola del Departamento de Agricultura de los Estados Unidos.

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

Si en algún momento no puedes avanzar…

Si en algún momento no puedes avanzar con este codelab o si deseas ver el estado final del código, puedes usar el siguiente vínculo:

Descargar código final

Como alternativa, puedes clonar el codelab completo de WorkManager desde GitHub:

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

Paso 1: Descarga el código

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

Download starting code

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

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

Paso 2: Obtén una imagen

Si usas un dispositivo en el que ya descargaste o tomaste fotos, ya puedes comenzar.

Si usas un dispositivo nuevo (como un emulador que se creó en forma reciente), te recomendamos que tomes una foto o descargues una imagen de la Web con el dispositivo. ¡Elige algo misterioso!

Paso 3: Ejecuta la app

Ejecuta la app. Deberías ver las siguientes pantallas (asegúrate de habilitar los permisos de acceso a las fotos a partir de la solicitud inicial y, si la imagen está inhabilitada, vuelve a abrir la app):


Puedes seleccionar una imagen e ir a la pantalla siguiente, que tiene botones de selección con los que podrás seleccionar cuánto desenfocar 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:

  • BlurApplication: Esta clase contiene la configuración de la aplicación.
  • WorkerUtils: Esta clase contiene el código de la acción de desenfoque y algunos métodos útiles que usarás más tarde para mostrar Notifications 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.
  • SelectImageActivity: Esta es la primera actividad que te permitirá seleccionar una imagen.
  • res/activity_blur.xml y res/activity_select.xml: Estos son los archivos de diseño de cada actividad.

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

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 {
    // Other dependencies
    implementation "androidx.work:work-runtime-ktx:$versions.work"
}

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

build.gradle

versions.work = "2.3.4"

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.

En este paso, tomarás una imagen de la carpeta res/drawable llamada test.jpg 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 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 res/test.jpg.

Anula el método doWork() y, luego, implementa lo siguiente:

  1. Obtén un Context llamando a la propiedad applicationContext. Lo necesitarás para las diferentes manipulaciones de mapas de bits que estás por hacer.
  2. Crea un Bitmap a partir de la imagen de prueba:
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.test)
  1. Obtén una versión desenfocada del mapa de bits llamando al método estático blurBitmap desde WorkerUtils.
  2. Escribe ese mapa de bits en un archivo temporal llamando al método estático 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 estático makeStatusNotification desde WorkerUtils.
  4. Muestra Result.success().
  5. Une el código de los pasos 2 a 6 en una sentencia try/catch. Captura un elemento Throwable genérico.
  6. En la sentencia de captura, emite una instrucción de registro de error: Timber.e(throwable, "Error applying blur").
  7. Luego, en la sentencia de captura, 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 androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R
import timber.log.Timber

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

            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) {
            Timber.e(throwable, "Error applying blur")
            Result.failure()
        }
    }
}

Paso 4: Obtén WorkManager en el ViewModel

Crea una variable 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 WorkRequests:

  • 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 applyBlur() de BlurViewModel:

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.


De manera opcional, puedes abrir el Explorador de archivos de dispositivos en Android Studio:

Luego, navega a data>data>com.example.background>files>blur_filter_outputs><URI> y confirma que el pez esté efectivamente desenfocado:


Desenfocar esa imagen de prueba está muy bien, pero para que Blur-O-Matic en verdad sea una app revolucionaria de edición de imágenes, deberás permitir que los usuarios desenfoquen sus propias imágenes.

Para hacerlo, proporcionaremos el URI de la imagen seleccionada del usuario como entrada en nuestro WorkRequest.

Paso 1: Crea el 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.

Crea un método privado llamado createInputDataForUri. Con este método harás lo siguiente:

  1. Crearás un objeto Data.Builder.
  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

Te recomendamos que cambies el método applyBlur para que realice lo siguiente:

  1. Cree una OneTimeWorkRequest.Builder nueva
  2. Llame a setInputData y pase el resultado de createInputDataForUri
  3. Compile el OneTimeWorkRequest
  4. Ponga en cola las solicitudes que usen WorkManager

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

Por medio del URI, puedes desenfocar la imagen que seleccionó el usuario:

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

        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)

        Result.success()
    } catch (throwable: Throwable) {
        Timber.e(throwable)
        Result.failure()
    }
}

Paso 5: URI temporal resultante

Ya que terminamos con este Trabajador, ahora podemos mostrar Result.success(). Proporcionaremos el OutputURI como un dato de salida para facilitar el acceso a esta imagen temporal a otros trabajadores a fin de realizar otras operaciones. Esto será útil en el siguiente capítulo, en el que crearemos una cadena de trabajadores. Para hacer lo siguiente:

  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

Ejecuta tu app. Debería compilar y tener el mismo comportamiento.

De manera opcional, puedes abrir el Explorador de archivos de dispositivos en Android Studio y navegar a 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:

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

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 WorkerRequests independientes que se ejecutan en orden o bien en paralelo. En este paso, crearás una cadena de trabajo que tiene el siguiente aspecto:

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 worker que extiende Worker.

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

Paso 2: Haz que extienda Worker

Agrega una dependencia a Worker para la clase CleanupWorker:

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. Dado que este no es un codelab sobre la manipulación de archivos, puedes copiar el código de CleanupWorker que aparece a continuación:

CleanupWorker.kt

package com.example.background.workers

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
import timber.log.Timber

/**
 * Cleans up temporary files generated during blurring process
 */
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()
                            Timber.i("Deleted $name - $deleted")
                        }
                    }
                }
            }
            Result.success()
        } catch (exception: Exception) {
            Timber.e(exception)
            Result.failure()
        }
    }
} 

Paso 4: Anula e implementa doWork() para SaveImageToFileWorker

SaveImageToFileWorker admitirá entradas y resultados. La entrada será una String almacenada con la clave KEY_IMAGE_URI. El resultado también será una String almacenada con la clave KEY_IMAGE_URI.

Dado que este no es un codelab sobre la manipulación de archivos, a continuación se incluye el código, con dos TODOs a fin de que los completes con el código apropiado para la entrada y el resultado. 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 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
import timber.log.Timber

/**
 * Saves the image to a permanent file
 */
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 {
                Timber.e("Writing to MediaStore failed")
                Result.failure()
            }
        } catch (exception: Exception) {
            Timber.e(exception)
            Result.failure()
        }
    }
}

Paso 5: Modifica la notificación de BlurWorker

Ahora que tenemos una cadena de Worker que se encarga de guardar la imagen en la carpeta correcta, podemos ralentizar el trabajo para que sea más fácil ver el inicio de cada 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) {
        Timber.e(throwable)
        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:

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. Deberías poder ver la imagen que elegiste desenfocar guardada en la carpeta Pictures:

Paso 7: Repite el BlurWorker

Es hora de agregar la capacidad de desenfocar la imagen reiteradas veces. Toma el parámetro blurLevel pasado 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()
}

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

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.

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<WorkInfo> 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<List<WorkInfo>> 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<List<WorkInfos>> 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. Declara una nueva variable llamada outputWorkInfos, que es un 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>>

// Add an init block to the BlurViewModel class
init {
    // 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 ocultará y mostrará las vistas adecuadas.
  5. Cuando termine, llama al objeto showWorkFinished(), que ocultará y mostrará las vistas correspondientes.

Este es el código:

BlurActivity.kt

// Show work status, added in onCreate()
viewModel.outputWorkInfos.observe(this, workInfosObserver())

// Add this functions
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:

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 el descriptor de acceso sintético 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.

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

// Put this inside onCreate()
// 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 as String)
                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:

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

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.

Por último, pero no menos importante, WorkManager admite Constraints. En el caso de Blur-O-Matic, usarás la restricción que establece que el dispositivo deberá estar cargándose cuando se realice la operación de guardado.

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 deseas y agregarlas a la WorkRequest, como se muestra a continuación:

BlurViewModel.kt

// Put this inside the applyBlur() function
// 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:

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

¡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 OneOffWorkRequest
  • 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:

Descargar código final

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.