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
- Conocimientos de StateFlow y ViewModel. Si no conoces estas clases, consulta el codelab ViewModel y State en Compose (específicamente para ViewModel y State) o el codelab Cómo leer y actualizar datos con Room (específicamente para Flow y StateFlow).
- Conocimientos sobre inserción de dependencias y repositorios. Si quieres repasar lo que aprendiste, consulta Cómo agregar un repositorio y una DI manual.
- Capacidad para implementar corrutinas en tu app.
Qué aprenderás
- Cómo agregar WorkManager a tu proyecto
- Cómo programar una tarea simple
- Cómo configurar los parámetros de entrada y salida para Workers
- Cómo encadenar Workers
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:
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:
- Abre el proyecto con el código de partida en Android Studio.
- Ejecuta la app en un dispositivo Android o en un emulador.
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 mostrarNotifications
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 conBlurViewModel
. 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étododoWork()
. 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. UnaWorkRequest
es donde defines si el Worker debe ejecutarse una vez o de forma periódica. También se pueden colocar restricciones en laWorkRequest
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 tuCoroutineWorker
como parte de la creación de tuWorkRequest
.WorkManager
: Esta clase programa tuWorkRequest
y la ejecuta. Programa unaWorkRequest
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.
- 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. - Asigna el nombre
BlurWorker
a la nueva clase de Kotlin. Extiéndelo desdeCoroutineWorker
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.
Si colocas el cursor sobre el texto class BlurWorker
, el IDE mostrará una ventana emergente con información adicional sobre el error.
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()
:
- Coloca el cursor dentro del código de la clase. Para ello, haz clic en el texto "BlurWorker".
- En el menú de Android Studio, selecciona Code > Override Methods…
- En la ventana emergente Override members, selecciona
doWork()
. - Haz clic en OK.
- Justo antes de la declaración de la clase, crea una variable llamada
TAG
y asígnale el valorBlurWorker
. Ten en cuenta que esta variable no está relacionada específicamente con el métododoWork()
, pero la usarás más adelante en llamadas aLog()
.
workers/BlurWorker.kt
private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
...
- Para ver mejor cuándo se ejecuta el trabajo, debes usar la función
makeStatusNotification()
deWorkerUtil
. 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
)
...
- 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) {
}
...
- En el bloque
try
, agrega una llamada aResult.success()
. - En el bloque
catch
, agrega una llamada aResult.failure()
.
workers/BlurWorker.kt
...
makeStatusNotification(
applicationContext.resources.getString(R.string.blurring_image),
applicationContext
)
return try {
Result.success()
} catch (throwable: Throwable) {
Result.failure()
}
...
- En el bloque
try
, crea una nueva variable llamadapicture
y propágala con el mapa de bits que se muestra después de llamar al métodoBitmapFactory.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()
...
- Desenfoca el mapa de bits llamando a la función
blurBitmap()
y pasa la variablepicture
y un valor de1
(uno) para el parámetroblurLevel
. - 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()
...
- Crea una nueva variable
outputUri
y propágala con una llamada a la funciónwriteBitmapToFile()
. - En la llamada a
writeBitmapToFile()
, pasa el contexto de la aplicación y la variableoutput
como argumentos.
workers/BlurWorker.kt
...
val output = blurBitmap(picture, 1)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(applicationContext, output)
Result.success()
...
- 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()
...
- En el bloque
catch
, registra un mensaje de error para indicar que se produjo un error al intentar desenfocar la imagen. La llamada aLog.e()
pasa la variableTAG
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.
- Crea un bloque
withContext()
. - Dentro de la llamada a
withContext()
, pasaDispatchers.IO
para que la función lambda se ejecute en un grupo de subprocesos especial para bloquear las operaciones de IO. - 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.
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.
- Dentro de la lambda
withContext()
, agrega una llamada a la función de utilidaddelay()
y pasa la constanteDELAY_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 claseWorkManagerBluromaticRepository
, crea una variable privada llamadaworkManager
y llama aWorkManager.getInstance(context)
para almacenar una instancia deWorkManager
.
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 unaWorkRequest
que solo se ejecuta una vez.PeriodicWorkRequest
: Es unaWorkRequest
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()
.
- Para propagar una variable nueva llamada
blurBuilder
, crea unaOneTimeWorkRequest
para el Worker de desenfoque y llama a la función de extensiónOneTimeWorkRequestBuilder
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>()
}
- Para iniciar el trabajo, llama al método
enqueue()
en tu objetoworkManager
.
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())
}
- 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.
Para confirmar que la imagen se haya desenfocado correctamente, puedes abrir Device Explorer en Android Studio:
Luego, navega a data > data > com.example.bluromatic > files > blur_filter_outputs > <URI> y confirma que la imagen de la magdalena esté desenfocada:
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.
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
- En el archivo
data/WorkManagerBluromaticRepository.kt
, dentro de la claseWorkManagerBluromaticRepository
, crea una nueva variable privada llamadaimageUri
. - 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()
.
- 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 auxiliarcreateInputDataForWorkRequest()
como argumento. Para la llamada a la funcióncreateInputDataForWorkRequest()
, pasa las variablesblurLevel
yimageUri
.
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()
:
- Crea una variable nueva llamada
resourceUri
y propaga la variable llamando ainputData.getString()
y pasando la constanteKEY_IMAGE_URI
que se usó como clave cuando se creó el objeto de datos de entrada.
val resourceUri = inputData.getString(KEY_IMAGE_URI)
- Crea una nueva variable llamada
blurLevel
. Para propagar la variable, llama ainputData.getInt()
y pasa la constanteBLUR_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 de1
(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.
- 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 sentenciarequire()
, que arroja unaIllegalArgumentException
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.
- Agrega un objeto
contentResolver
al valorapplicationContext
.
workers/BlurWorker.kt
...
require(!resourceUri.isNullOrBlank()) {
// ...
}
val resolver = applicationContext.contentResolver
...
- Debido a que ahora la fuente de la imagen es el URI que se pasó, usa
BitmapFactory.decodeStream()
en lugar deBitmapFactory.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))
)
- Pasa la variable
blurLevel
en la llamada a la funciónblurBitmap()
.
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:
- Antes del código
Result.success()
, crea una nueva variable llamadaoutputData
. - Para propagar esta variable, llama a la función
workDataOf()
y usa la constanteKEY_IMAGE_URI
para la clave y la variableoutputUri
como valor. La funciónworkDataOf()
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())
- Actualiza el código
Result.success()
para tomar este nuevo objeto de datos como argumento.
workers/BlurWorker.kt
//Result.success()
Result.success(outputData)
- 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:
¡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:
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.
- 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. - Asigna el nombre
CleanupWorker
a la nueva clase de Kotlin. - 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
.
- 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. - Asigna el nombre
SaveImageToFileWorker
a la nueva clase de Kotlin. - 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.
- En lugar de llamar a
OneTimeWorkRequestBuilder
, llama aworkManager.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
.
- Quita la llamada a
workManager.enqueue(blurBuilder.build())
, que solo estaba poniendo en cola una solicitud de trabajo. - 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())
...
- 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)
...
- 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
.
- 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.
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.
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.