Trabajo en segundo plano con WorkManager

1. Antes de comenzar

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, que garantiza su ejecución.

Requisitos previos

Qué aprenderás

Actividades

  • Modificar una app de partida para usar WorkManager
  • Implementar una solicitud de trabajo para desenfocar una imagen
  • Implementar un grupo de trabajo en serie al encadenar un trabajo
  • Pasar datos hacia y desde el trabajo que se está programando

Requisitos

  • La versión estable más reciente de Android Studio
  • Conexión a Internet

2. Descripción general de la app

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

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

La pantalla tiene botones de selección que te permiten elegir el grado de desenfoque de la imagen. Haz clic en el botón Start para desenfocar y guardar la imagen.

Por el momento, la app no desenfoca ni guarda la imagen final.

Este codelab se enfoca en agregar WorkManager a la app, crear trabajadores para limpiar archivos temporales que se crean para desenfocar una imagen, desenfocar una imagen y guardar una copia final de la imagen, que puedes ver cuando haces clic en el botón See File. También aprenderás a supervisar el estado del trabajo en segundo plano y a actualizar la IU de la app según corresponda.

3. Explora la app de partida de Blur-O-Matic

Obtén el código de partida

Para comenzar, descarga el código de partida:

Descargar ZIP

Como alternativa, puedes clonar el repositorio de GitHub para el código:

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

Puedes explorar el código de la app de Blur-O-Matic en este repositorio de GitHub.

Ejecuta el código de partida

Para familiarizarte con el código de partida, completa los siguientes pasos:

  1. Abre el proyecto con el código de partida en Android Studio.
  2. Ejecuta la app en un dispositivo Android o en un emulador.

2bdb6fdc2567e96.png

La pantalla tiene botones de selección que te permiten elegir la cantidad de desenfoque de la imagen. Cuando haces clic en el botón Start, la app se desenfoca y guarda la imagen.

Por el momento, la app no aplica ningún desenfoque al hacer clic en el botón Start.

Explicación del código de partida

En esta tarea, te familiarizarás con la estructura del proyecto. En las siguientes listas, se proporcionan explicaciones de los archivos y las carpetas importantes del proyecto.

  • WorkerUtils: Métodos de conveniencia que más tarde usarás para mostrar Notifications y código para guardar un mapa de bits en un archivo.
  • BlurViewModel: Este modelo de vista almacena el estado de la app e interactúa con el repositorio.
  • WorkManagerBluromaticRepository: Es la clase en la que inicias el trabajo en segundo plano con WorkManager.
  • Constants: Es una clase estática con algunas constantes que usas durante el codelab.
  • BluromaticScreen: Contiene funciones componibles para la IU e interactúa con BlurViewModel. Las funciones de componibilidad muestran la imagen y tienen botones de selección para elegir el nivel de desenfoque deseado.

4. ¿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 quiere decir que WorkManager realiza el trabajo en segundo plano tan pronto como sea posible. La ejecución garantizada implica que WorkManager se encarga de la lógica para iniciar tu trabajo en diferentes situaciones, incluso si sales de la app.

WorkManager es una biblioteca extremadamente flexible que cuenta con muchos beneficios adicionales. Algunos de estos beneficios son los siguientes:

  • 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, como la ejecución de trabajos 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).
  • Permite trabajar con o sin los Servicios de Google Play.
  • Sigue las prácticas recomendadas sobre el estado del sistema.
  • Ofrece compatibilidad para mostrar fácilmente el estado de solicitudes de trabajo en la IU de la app.

5. Cuándo usar WorkManager

La biblioteca de WorkManager es una buena opción para las tareas que debes completar. La ejecución de estas tareas no depende de que la app continúe ejecutándose después de poner el trabajo en cola. Las tareas se ejecutan incluso si se cierra la app o si el usuario vuelve a la pantalla principal.

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

  • Consultar periódicamente las noticias más recientes
  • Aplicar filtros a una imagen y guardarla
  • Sincronizar datos locales con la red de forma periódica

WorkManager es una opción para ejecutar una tarea fuera del subproceso principal, pero no es una acción genérica a la hora de ejecutar cada tipo de tarea fuera del subproceso principal. Las corrutinas son otra opción que analizan los codelabs anteriores.

Si quieres obtener más información sobre el uso de WorkManager, consulta la Guía sobre el trabajo en segundo plano.

6. Agrega WorkManager a tu app

WorkManager requiere la siguiente dependencia de Gradle. Esto ya se incluye en el archivo de compilación:

app/build.gradle.kts

dependencies {
    // WorkManager dependency
    implementation("androidx.work:work-runtime-ktx:2.8.1")
}

Debes usar la versión estable más actualizada de work-runtime-ktx en tu app.

Si cambias la versión, asegúrate de hacer clic en Sync Now para sincronizar tu proyecto con los archivos de Gradle actualizados.

7. Aspectos básicos de WorkManager

Existen algunas clases de WorkManager que debes conocer:

  • Worker/CoroutineWorker: Worker es una clase que trabaja de manera síncrona en un subproceso en segundo plano. Como nos interesa el trabajo asíncrono, podemos usar CoroutineWorker, que tiene interoperabilidad con las corrutinas de Kotlin. En esta app, te extenderás respecto de la clase CoroutineWorker y anularás el método doWork(). Este método es donde colocas el código del trabajo real que deseas realizar en segundo plano.
  • WorkRequest: Esta clase representa una solicitud para realizar algunos trabajos. Una WorkRequest es donde defines si el Worker debe ejecutarse una vez o de forma periódica. También se pueden colocar restricciones en la WorkRequest que requieren que se cumplan ciertas condiciones antes de que se ejecute el trabajo. Un ejemplo es que el dispositivo se está cargando antes de iniciar el trabajo solicitado. Debes pasar tu CoroutineWorker como parte de la creación de tu WorkRequest.
  • WorkManager: Esta clase programa tu WorkRequest y la ejecuta. Programa una WorkRequest de manera que se distribuya la carga sobre los recursos del sistema, respetando las restricciones que hayas especificado.

En tu caso, define una nueva clase BlurWorker, que contiene el código para desenfocar una imagen. Cuando haces clic en el botón Start, WorkManager crea un objeto WorkRequest y lo pone en cola.

8. Crea el BlurWorker

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 desenfocan la imagen.

  1. Haz clic con el botón derecho en el paquete com.example.bluromatic.workers en el panel de tu proyecto de Android y selecciona New -> Kotlin Class/File.
  2. Asigna el nombre BlurWorker a la nueva clase de Kotlin. Extiéndelo desde CoroutineWorker con los parámetros de constructor requeridos.

workers/BlurWorker.kt

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

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

La clase BlurWorker extiende la clase CoroutineWorker en lugar de la clase Worker más general. La implementación de la clase CoroutineWorker de doWork() es una función de suspensión, que le permite ejecutar código asíncrono que un Worker no puede hacer. Como se detalla en la guía sobre ejecución de subprocesos en WorkManager, "CoroutineWorker es la implementación recomendada para los usuarios de Kotlin".

En este punto, Android Studio dibuja una línea ondulada roja debajo de class BlurWorker que indica un error.

9e96aa94f82c6990.png

Si colocas el cursor sobre el texto class BlurWorker, el IDE mostrará una ventana emergente con información adicional sobre el error.

cdc4bbefa7a9912b.png

El mensaje de error indica que no anulaste el método doWork() como se solicitó.

En el método doWork(), escribe el código para desenfocar la imagen de la magdalena que se muestra.

Sigue estos pasos para corregir el error e implementar el método doWork():

  1. Coloca el cursor dentro del código de la clase. Para ello, haz clic en el texto "BlurWorker".
  2. En el menú de Android Studio, selecciona Code > Override Methods…
  3. En la ventana emergente Override members, selecciona doWork().
  4. Haz clic en OK.

8f495f0861ed19ff.png

  1. Justo antes de la declaración de la clase, crea una variable llamada TAG y asígnale el valor BlurWorker. Ten en cuenta que esta variable no está relacionada específicamente con el método doWork(), pero la usarás más adelante en llamadas a Log().

workers/BlurWorker.kt

private const val TAG = "BlurWorker"

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
... 
  1. Para ver mejor cuándo se ejecuta el trabajo, debes usar la función makeStatusNotification() de WorkerUtil. Esta función te permite mostrar fácilmente un banner de notificación en la parte superior de la pantalla.

Dentro del método doWork(), usa la función makeStatusNotification() para mostrar una notificación de estado y notificar al usuario que se inició el Worker de desenfoque y que está desenfocando la imagen.

workers/BlurWorker.kt

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

    makeStatusNotification(
        applicationContext.resources.getString(R.string.blurring_image),
        applicationContext
    )
...
  1. Agrega un bloque de código return try...catch, que es donde se realiza el verdadero trabajo de desenfoque de la imagen.

workers/BlurWorker.kt

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

        return try {
        } catch (throwable: Throwable) {
        }
...
  1. En el bloque try, agrega una llamada a Result.success().
  2. En el bloque catch, agrega una llamada a Result.failure().

workers/BlurWorker.kt

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

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...
  1. En el bloque try, crea una nueva variable llamada picture y propágala con el mapa de bits que se muestra después de llamar al método BitmapFactory.decodeResource() y pasar el paquete de recursos de la aplicación y el ID de recurso de la imagen de la magdalena.

workers/BlurWorker.kt

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

            Result.success()
...
  1. Desenfoca el mapa de bits llamando a la función blurBitmap() y pasa la variable picture y un valor de 1 (uno) para el parámetro blurLevel.
  2. Guarda el resultado en una variable nueva llamada output.

workers/BlurWorker.kt

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

            val output = blurBitmap(picture, 1)

            Result.success()
...
  1. Crea una nueva variable outputUri y propágala con una llamada a la función writeBitmapToFile().
  2. En la llamada a writeBitmapToFile(), pasa el contexto de la aplicación y la variable output como argumentos.

workers/BlurWorker.kt

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

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

            Result.success()
...
  1. Agrega código para mostrar al usuario un mensaje de notificación que contenga la variable outputUri.

workers/BlurWorker.kt

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

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...
  1. En el bloque catch, registra un mensaje de error para indicar que se produjo un error al intentar desenfocar la imagen. La llamada a Log.e() pasa la variable TAG definida con anterioridad, un mensaje apropiado y la excepción que se arroja.

workers/BlurWorker.kt

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

Un CoroutineWorker, se ejecuta de forma predeterminada como Dispatchers.Default, pero se puede cambiar si se llama a withContext() y se pasa el despachador deseado.

  1. Crea un bloque withContext().
  2. Dentro de la llamada a withContext(), pasa Dispatchers.IO para que la función lambda se ejecute en un grupo de subprocesos especial para bloquear las operaciones de IO.
  3. Mueve el código return try...catch que se escribió anteriormente a este bloque.
...
        return withContext(Dispatchers.IO) {

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

Android Studio muestra el siguiente error porque no puedes llamar a return desde una función lambda.

2d81a484b1edfd1d.png

Para corregir este error, agrega una etiqueta como se muestra en la ventana emergente.

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

Debido a que este Worker se ejecuta muy rápidamente, se recomienda agregar una demora en el código para emular un trabajo que se ejecuta con mayor lentitud.

  1. Dentro de la lambda withContext(), agrega una llamada a la función de utilidad delay() y pasa la constante DELAY_TIME_MILLIS. Esta llamada sirve estrictamente para que el codelab proporcione un retraso entre los mensajes de notificación.
import com.example.bluromatic.DELAY_TIME_MILLIS
import kotlinx.coroutines.delay

...
        return withContext(Dispatchers.IO) {

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

                val picture = BitmapFactory.decodeResource(
...

9. Actualiza WorkManagerBluromaticRepository

El repositorio controla todas las interacciones con WorkManager. Esta estructura cumple con el principio de diseño de separación de problemas y es un patrón de arquitectura de Android recomendado.

  • En el archivo data/WorkManagerBluromaticRepository.kt, dentro de la clase WorkManagerBluromaticRepository, crea una variable privada llamada workManager y llama a WorkManager.getInstance(context) para almacenar una instancia de WorkManager.

data/WorkManagerBluromaticRepository.kt

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

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

Crea la WorkRequest y colócala en cola en WorkManager

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

  • OneTimeWorkRequest: Es una WorkRequest que solo se ejecuta una vez.
  • PeriodicWorkRequest: Es una WorkRequest que se ejecuta de manera repetida en un ciclo.

Solo quieres desenfocar la imagen una vez cuando haces clic en el botón Start.

Este trabajo se lleva a cabo en el método applyBlur(), al que llamas cuando haces clic en el botón Start.

Los siguientes pasos se completan dentro del método applyBlur().

  1. Para propagar una variable nueva llamada blurBuilder, crea una OneTimeWorkRequest para el Worker de desenfoque y llama a la función de extensión OneTimeWorkRequestBuilder desde WorkManager KTX.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
}
  1. Para iniciar el trabajo, llama al método enqueue() en tu objeto workManager.

data/WorkManagerBluromaticRepository.kt

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

    // Start the work
    workManager.enqueue(blurBuilder.build())
}
  1. Ejecuta la app y ve la notificación cuando haces clic en el botón Start.

En este momento, la imagen se desenfoca del mismo modo, independientemente de la opción que selecciones. En pasos posteriores, la cantidad de desenfoque cambia según la opción seleccionada.

f2b3591b86d1999d.png

Para confirmar que la imagen se haya desenfocado correctamente, puedes abrir Device Explorer en Android Studio:

6bc555807e67f5ad.png

Luego, navega a data > data > com.example.bluromatic > files > blur_filter_outputs > <URI> y confirma que la imagen de la magdalena esté desenfocada:

fce43c920a61a2e3.png

10. Datos de entrada y datos de salida

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

Para ello, proporcionamos el URI de la imagen de la magdalena que se muestra como entrada a nuestra WorkRequest y, luego, usamos la salida de la WorkRequest para mostrar la imagen desenfocada final.

ce8ec44543479fe5.png

La entrada y la salida pasan por un trabajador en la dirección correspondiente a través de objetos Data. Los objetos Data son contenedores livianos para pares clave-valor. Se diseñaron para almacenar una pequeña cantidad de datos que pueden entrar y salir de un trabajador desde la WorkRequest.

En el siguiente paso, crearás un objeto de datos de entrada para pasar el URI a BlurWorker.

Crea el objeto de datos de entrada

  1. En el archivo data/WorkManagerBluromaticRepository.kt, dentro de la clase WorkManagerBluromaticRepository, crea una nueva variable privada llamada imageUri.
  2. Propaga la variable con el URI de la imagen mediante una llamada al método contextual getImageUri().

data/WorkManagerBluromaticRepository.kt

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

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

El código de la app contiene la función auxiliar createInputDataForWorkRequest() para crear objetos de datos de entrada.

data/WorkManagerBluromaticRepository.kt

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

Primero, la función auxiliar crea un objeto Data.Builder. Luego, coloca imageUri y blurLevel en el objeto como pares clave-valor. A continuación, se crea un objeto de datos y se muestra cuando llama a return builder.build().

  1. Para establecer el objeto de datos de entrada para WorkRequest, llama al método blurBuilder.setInputData(). Puedes crear y pasar el objeto de datos en un solo paso llamando a la función auxiliar createInputDataForWorkRequest() como argumento. Para la llamada a la función createInputDataForWorkRequest(), pasa las variables blurLevel y imageUri.

data/WorkManagerBluromaticRepository.kt

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

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

    workManager.enqueue(blurBuilder.build())
}

Accede al objeto de datos de entrada

Ahora, actualicemos el método doWork() en la clase BlurWorker para obtener el URI y el nivel de desenfoque que pasó el objeto de datos de entrada. Si no se proporcionó un valor para blurLevel, el valor predeterminado será 1.

Dentro del método doWork():

  1. Crea una variable nueva llamada resourceUri y propaga la variable llamando a inputData.getString() y pasando la constante KEY_IMAGE_URI que se usó como clave cuando se creó el objeto de datos de entrada.

val resourceUri = inputData.getString(KEY_IMAGE_URI)

  1. Crea una nueva variable llamada blurLevel. Para propagar la variable, llama a inputData.getInt() y pasa la constante BLUR_LEVEL que se usó como clave cuando se creó el objeto de datos de entrada. En caso de que no se haya creado este par clave-valor, proporciona un valor predeterminado de 1 (uno).

workers/BlurWorker.kt

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

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

    // ... rest of doWork()
}

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

  1. Verifica que la variable resourceUri esté propagada. Si no se propaga, el código debería arrojar una excepción. El siguiente código usa la sentencia require(), que arroja una IllegalArgumentException si el primer argumento se evalúa como falso.

workers/BlurWorker.kt

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

Dado que la fuente de la imagen se pasa como un URI, necesitamos un objeto ContentResolver para leer el contenido al que apunta el URI.

  1. Agrega un objeto contentResolver al valor applicationContext.

workers/BlurWorker.kt

...
    require(!resourceUri.isNullOrBlank()) {
        // ...
    }
    val resolver = applicationContext.contentResolver
...
  1. Debido a que ahora la fuente de la imagen es el URI que se pasó, usa BitmapFactory.decodeStream() en lugar de BitmapFactory.decodeResource() para crear el objeto Bitmap.

workers/BlurWorker.kt

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

    val resolver = applicationContext.contentResolver

    val picture = BitmapFactory.decodeStream(
        resolver.openInputStream(Uri.parse(resourceUri))
    )
  1. Pasa la variable blurLevel en la llamada a la función blurBitmap().

workers/BlurWorker.kt

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

Crea el objeto de datos de salida

Ya terminaste con este Worker y puedes mostrar el URI de salida como un objeto de datos de salida en Result.success(). Cuando se brinda el URI de salida como un objeto de datos de salida, se facilita el acceso a otros Workers para operaciones adicionales. Este enfoque es útil en la próxima sección, dedicada a la creación de una cadena de trabajadores.

Para ello, sigue los pasos que se indican a continuación:

  1. Antes del código Result.success(), crea una nueva variable llamada outputData.
  2. Para propagar esta variable, llama a la función workDataOf() y usa la constante KEY_IMAGE_URI para la clave y la variable outputUri como valor. La función workDataOf() crea un objeto de datos a partir del par clave-valor que se pasó.

workers/BlurWorker.kt

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
  1. Actualiza el código Result.success() para tomar este nuevo objeto de datos como argumento.

workers/BlurWorker.kt

//Result.success()
Result.success(outputData)
  1. Quita el código que muestra la notificación. Ya no es necesario porque el objeto de datos de salida ahora usa el URI.

workers/BlurWorker.kt

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

Ejecuta tu app

En este punto, cuando ejecutas tu app, puedes esperar que se compile. Puedes ver la imagen desenfocada en Device Explorer, pero todavía no en la pantalla.

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

a658ad6e65f0ce5d.png

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

11. Encadena tu trabajo

De momento, estás realizando una sola tarea: desenfocar la imagen. Esta tarea es un excelente primer paso, pero aún faltan algunas funcionalidades principales en la app, como las siguientes:

  • La app no borra archivos temporales.
  • La app no guarda la imagen en un archivo permanente.
  • La app siempre desenfoca la imagen de la misma manera.

Puedes usar una cadena de trabajo de WorkManager para agregar esta funcionalidad. WorkManager te permite crear WorkerRequest independientes que se ejecutan en orden o en paralelo.

En esta sección, crearás una cadena de trabajo que se verá de la siguiente manera:

c883bea5a5beac45.png

Los cuadros representan las WorkRequest.

Otra función del encadenamiento es su capacidad de aceptar entradas y producir salidas. La salida de una WorkRequest se convierte en la entrada de la siguiente WorkRequest en la cadena.

Ya tienes un CoroutineWorker para desenfocar una imagen, pero también necesitas un CoroutineWorker para limpiar los archivos temporales y un CoroutineWorker para guardar la imagen de forma permanente.

Crea un CleanupWorker

CleanupWorker borra los archivos temporales, si los hay.

  1. Haz clic con el botón derecho en el paquete com.example.bluromatic.workers en el panel de tu proyecto de Android y selecciona New -> Kotlin Class/File.
  2. Asigna el nombre CleanupWorker a la nueva clase de Kotlin.
  3. Copia el código de CleanupWorker.kt, como se muestra en el siguiente ejemplo de código.

Como la manipulación de archivos está fuera del alcance de este codelab, puedes copiar el siguiente código para CleanupWorker.

workers/CleanupWorker.kt

package com.example.bluromatic.workers

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

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

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

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

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

            return@withContext try {
                val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
                if (outputDirectory.exists()) {
                    val entries = outputDirectory.listFiles()
                    if (entries != null) {
                        for (entry in entries) {
                            val name = entry.name
                            if (name.isNotEmpty() && name.endsWith(".png")) {
                                val deleted = entry.delete()
                                Log.i(TAG, "Deleted $name - $deleted")
                            }
                        }
                    }
                }
                Result.success()
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_cleaning_file),
                    exception
                )
                Result.failure()
            }
        }
    }
}

Crea una clase SaveImageToFileWorker

La clase SaveImageToFileWorker guarda el archivo temporal en un archivo permanente.

SaveImageToFileWorker toma entradas y salidas. La entrada es una String del URI de la imagen desenfocada temporalmente y almacenada con la clave KEY_IMAGE_URI. La salida es una String del URI de la imagen desenfocada guardada, almacenada con la clave KEY_IMAGE_URI.

de0ee97cca135cf8.png

  1. Haz clic con el botón derecho en el paquete com.example.bluromatic.workers en el panel de tu proyecto de Android y selecciona New -> Kotlin Class/File.
  2. Asigna el nombre SaveImageToFileWorker a la nueva clase de Kotlin.
  3. Copia el código SaveImageToFileWorker.kt como se muestra en el siguiente código de ejemplo.

Como la manipulación de archivos está fuera del alcance de este codelab, puedes copiar el siguiente código para SaveImageToFileWorker. En el código proporcionado, observa cómo se recuperan y almacenan los valores resourceUri y output con la clave KEY_IMAGE_URI. Este proceso es muy similar al código que escribiste anteriormente para los objetos de datos de entrada y salida.

workers/SaveImageToFileWorker.kt

package com.example.bluromatic.workers

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

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

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

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

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

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

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

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

Crea una cadena de trabajo

Actualmente, el código solo crea y ejecuta una única WorkRequest.

En este paso, modificarás el código para crear y ejecutar una cadena de WorkRequests, en lugar de solo una solicitud de imagen de desenfoque.

En la cadena de WorkRequests, tu primera solicitud de trabajo consiste en limpiar los archivos temporales.

  1. En lugar de llamar a OneTimeWorkRequestBuilder, llama a workManager.beginWith().

El llamado al método beginWith() muestra un objeto WorkContinuation y crea el punto de partida para una cadena de WorkRequest con la primera solicitud de trabajo en la cadena.

data/WorkManagerBluromaticRepository.kt

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

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

Para agregar a esta cadena de solicitudes de trabajo, llama al método then() y pasa un objeto WorkRequest.

  1. Quita la llamada a workManager.enqueue(blurBuilder.build()), que solo estaba poniendo en cola una solicitud de trabajo.
  2. Agrega la siguiente solicitud de trabajo a la cadena llamando al método .then().

data/WorkManagerBluromaticRepository.kt

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

// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
  1. Crea una solicitud de trabajo para guardar la imagen y agregarla a la cadena.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.SaveImageToFileWorker

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

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .build()
continuation = continuation.then(save)
...
  1. Para iniciar el trabajo, llama al método enqueue() en el objeto de continuación.

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

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

Este código produce y ejecuta la siguiente cadena de WorkRequests: una WorkRequest de CleanupWorker seguida de una WorkRequest de BlurWorker seguida de una WorkRequest de SaveImageToFileWorker.

  1. Ejecuta la app.

Ahora puedes hacer clic en Start y ver las notificaciones cuando se ejecuten los distintos trabajadores. Todavía puedes ver la imagen desenfocada en Device Explorer. En una próxima sección, agregarás un botón adicional para que los usuarios puedan ver la imagen desenfocada en el dispositivo.

En las siguientes capturas de pantalla, observa que el mensaje de notificación muestra qué Worker se está ejecutando actualmente.

bbe0fdd79e3bca27.png

5d43bbfff1bfebe5.png

da2d31fa3609a7b1.png

Observa que la carpeta de salida contiene varias imágenes desenfocadas. Podrás ver imágenes que se encuentran en etapas intermedias de desenfoque y la versión final que muestra la imagen con la cantidad de desenfoque que seleccionaste.

¡Buen trabajo! Ahora puedes limpiar los archivos temporales, desenfocar una imagen y guardarla.

12. Obtén el código de la solución

Para descargar el código del codelab terminado, puedes usar estos comandos:

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

También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.

Descargar ZIP

Si deseas ver el código de la solución para este codelab, míralo en GitHub.

13. Conclusión

¡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

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 Cómo programar tareas con WorkManager.