1. Introducción
En Android, hay muchas opciones para realizar trabajos diferibles en segundo plano. Este codelab trata sobre WorkManager, una biblioteca con retrocompatibilidad, flexible y simple para realizar trabajos diferibles en segundo plano. WorkManager es el programador de tareas recomendado de Android para realizar trabajos diferibles, lo que garantiza su ejecución.
¿Qué es WorkManager?
WorkManager es parte de Android Jetpack y un componente de la arquitectura para trabajos en segundo plano que requieren una ejecución tanto oportunista como garantizada. La ejecución oportunista implica que WorkManager realizará el trabajo en segundo plano tan pronto como sea posible. La ejecución garantizada implica que WorkManager se encargará de la lógica a los efectos de iniciar tu trabajo en diferentes situaciones, incluso si sales de la app.
WorkManager es una biblioteca extremadamente flexible que cuenta con muchos beneficios adicionales. Por ejemplo:
- Es compatible con tareas asíncronas únicas y periódicas.
- Admite restricciones, como condiciones de red, espacio de almacenamiento y estado de carga.
- Encadena solicitudes de trabajo complejas, incluido el trabajo de ejecución en paralelo.
- Usa el resultado de una solicitud de trabajo como entrada para la siguiente.
- Controla la compatibilidad con el nivel de API 14 (consulta la nota).
- Trabaja con o sin los Servicios de Google Play.
- Sigue las prácticas recomendadas sobre el estado del sistema.
- Ofrece compatibilidad con LiveData a fin de mostrar fácilmente el estado de la solicitud de trabajo en la IU.
Cuándo usar WorkManager
La biblioteca de WorkManager es una buena opción para las tareas que resultan útiles de completar, incluso si el usuario sale de una pantalla en particular o de tu app.
Algunos ejemplos de tareas que muestran un buen uso de WorkManager:
- Subir registros
- Aplicar filtros a imágenes y guardar la imagen
- Sincronizar datos locales con la red de forma periódica
WorkManager ofrece una ejecución garantizada, y no todas las tareas lo necesitan. Por consiguiente, su función no es la de ejecutar todas las tareas del subproceso principal. Si quieres obtener más información sobre el uso de WorkManager, consulta la Guía para el procesamiento en segundo plano.
Qué compilarás
Hoy en día, los smartphones son muy buenos para tomar fotos. Atrás quedaron los días en que un fotógrafo tomaba fotos desenfocadas de objetos misteriosos.
En este codelab, trabajarás en Blur-O-Matic, una app que desenfoca imágenes y fotos, y guarda el resultado en un archivo. ¿Eso era el monstruo del Lago Ness o un submarino de juguete? Con Blur-O-Matic, nadie lo sabrá jamás.
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
- Contar con la versión estable más reciente de Android Studio
- Conocer
LiveData
yViewModel
(si no estás familiarizado con estas clases, consulta el Codelab de componentes optimizados para ciclos de vida de Android, en especial para ViewModel y LiveData, o el Codelab sobre Room con un elemento View, una introducción a los componentes de la arquitectura)
2. Cómo prepararte
Paso 1: Descarga el código
Haz clic en el siguiente vínculo a fin de descargar todo el código de este codelab:
Si lo prefieres, también puedes clonar el codelab de WorkManager desde GitHub:
$ git clone -b start_kotlin https://github.com/googlecodelabs/android-workmanager
Paso 2: Ejecuta la app
Ejecuta la app. Deberías ver la siguiente pantalla:
La pantalla debe tener botones de selección con los que puedes elegir cuánto desenfocarás tu imagen. Si presionas el botón Go, la imagen se desenfocará y se guardará.
A partir de este momento, la app dejará de desenfocar la imagen.
El código inicial contiene lo siguiente:
WorkerUtils
: Esta clase contiene el código para desenfocar una imagen y algunos métodos útiles que usarás más tarde para mostrarNotifications
, guardar un mapa de bits para archivar y ralentizar la app.BlurActivity
:* Esta es la actividad que muestra la imagen e incluye botones de selección para elegir el grado de desenfoque.BlurViewModel
:* Este modelo de vista almacena todos los datos necesarios a fin de mostrar laBlurActivity
. También será la clase en la que inicies el trabajo en segundo plano con WorkManager.Constants
: Esta es una clase estática con algunas constantes que usarás durante el codelab.res/activity_blur.xml
: Los archivos de diseño paraBlurActivity
.
***** Estos son los únicos archivos en los que escribirás código.
3. Agrega WorkManager a tu app
WorkManager
requiere la dependencia de Gradle que se indica a continuación. Estas ya se incluyeron en los archivos de compilación:
app/build.gradle
dependencies {
// WorkManager dependency
implementation "androidx.work:work-runtime-ktx:$versions.work"
}
Deberás obtener la versión estable más reciente de work-runtime-ktx
aquí y colocar la versión correcta. En este momento, la versión más reciente es:
build.gradle
versions.work = "2.7.1"
Si actualizas tu versión a una más reciente, asegúrate de usar la opción Sincronizar ahora a fin de sincronizar tu proyecto con los archivos de Gradle modificados.
4. Crea tu primera WorkRequest
En este paso, tomarás una imagen de la carpeta res/drawable
llamada android_cupcake.png
y ejecutarás algunas funciones sobre ella en segundo plano. Estas funciones desenfocarán la imagen y la guardarán en un archivo temporal.
Aspectos básicos de WorkManager
Existen algunas clases de WorkManager que debes conocer:
Worker
: Aquí es donde colocas el código del trabajo real que deseas realizar en segundo plano. Extenderás esta clase y anularás el métododoWork()
.WorkRequest
: Esta clase representa una solicitud para realizar algunos trabajos. Como parte de la creación de tuWorkRequest
, pasarás elWorker
. Cuando hagas laWorkRequest
, también podrás especificar elementos comoConstraints
sobre el momento en que se debe ejecutar elWorker
.WorkManager
: Esta clase programa tuWorkRequest
y la ejecuta. ProgramaWorkRequest
s de manera que se distribuya la carga sobre los recursos del sistema, respetando las restricciones que hayas especificado.
En tu caso, definirás un nuevo BlurWorker
que contendrá el código para desenfocar una imagen. Cuando se haga clic en el botón Go, se creará una WorkRequest
y, luego, WorkManager
lo pondrá en cola.
Paso 1: Crea el BlurWorker
En el paquete workers
, crea una nueva clase de Kotlin llamada BlurWorker
.
Paso 2: Agrega un constructor
Agrega una dependencia a Worker
para la clase BlurWorker
:
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}
Paso 3: Anula e implementa doWork()
Tu Worker
desenfocará la imagen de la magdalena que se muestra.
Para ver mejor cuándo se ejecuta el trabajo, usarás la clase makeStatusNotification()
de WorkerUtil. Este método te permitirá ver fácilmente un banner de notificación en la parte superior de la pantalla.
Anula el método doWork()
y, luego, implementa lo siguiente. Puedes consultar el código completo al final de la sección:
- Obtén un
Context
llamando a la propiedadapplicationContext
. Asígnalo a un nuevoval
llamadoappContext
. Lo necesitarás para las diferentes manipulaciones de mapas de bits que harás. - Muestra una notificación de estado con la función
makeStatusNotification
para notificar al usuario sobre el desenfoque de la imagen. - Crea un
Bitmap
a partir de la imagen de la magdalena:
val picture = BitmapFactory.decodeResource(
appContext.resources,
R.drawable.android_cupcake)
- Obtén una versión desenfocada del mapa de bits llamando al método
blurBitmap
desdeWorkerUtils
. - Escribe ese mapa de bits en un archivo temporal llamando al método
writeBitmapToFile
desdeWorkerUtils
. Asegúrate de guardar el URI que se muestra en una variable local. - Realiza una notificación en la que se muestre el URI llamando al método
makeStatusNotification
desdeWorkerUtils
. - Muestra
Result.success()
. - Une el código de los pasos 3 a 6 en una sentencia try/catch. Captura un elemento
Throwable
genérico. - En la sentencia catch, imprime un mensaje de error con la instrucción de registro
Log.e(TAG, "Error applying blur")
. - Luego, en la sentencia catch, muestra
Result.failure()
.
A continuación, se incluye el código completo correspondiente a este paso.
**BlurWorker.**kt
package com.example.background.workers
import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R
private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val appContext = applicationContext
makeStatusNotification("Blurring image", appContext)
return try {
val picture = BitmapFactory.decodeResource(
appContext.resources,
R.drawable.android_cupcake)
val output = blurBitmap(picture, appContext)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(appContext, output)
makeStatusNotification("Output is $outputUri", appContext)
Result.success()
} catch (throwable: Throwable) {
Log.e(TAG, "Error applying blur")
Result.failure()
}
}
}
Paso 4: Obtén WorkManager en el ViewModel
Crea una variable de clase para una instancia WorkManager
en tu ViewModel
:
BlurViewModel.kt
private val workManager = WorkManager.getInstance(application)
Paso 5: Pon en cola la WorkRequest en WorkManager
Es hora de crear una WorkRequest
y pedirle a WorkManager que la ejecute. Existen dos tipos de WorkRequest
:
OneTimeWorkRequest
: Es unaWorkRequest
que solo se ejecutará una vez.PeriodicWorkRequest
: Es unaWorkRequest
que se repetirá en forma cíclica.
Solo queremos que la imagen se desenfoque una vez cuando se haga clic en el botón Go. Se llamará al método applyBlur
cuando se haga clic en el botón Go, por lo que deberás crear una OneTimeWorkRequest
desde BlurWorker
. Luego, usa tu instancia de WorkManager
para poner en cola tu WorkRequest.
Agrega la siguiente línea de código al método BlurViewModel's
applyBlur()
:
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
workManager.enqueue(OneTimeWorkRequest.from(BlurWorker::class.java))
}
Paso 6: Ejecuta el código
Ejecuta tu código. Debería compilarse, y tú deberías ver la notificación cuando presiones el botón Go. Ten en cuenta que, para ver un resultado más desenfocado, debes seleccionar las opciones “More blurred” o “The most blurred”.
Para confirmar que la imagen se desenfocó correctamente, 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 la magdalena sí esté desenfocada:
5. Agrega parámetros de entrada y salida
Desenfocar el elemento de imagen en el directorio de recursos está muy bien, pero para que Blur-O-Matic en verdad sea una app revolucionaria de edición de imágenes, debes permitir que el usuario desenfoque la imagen que ve en la pantalla y que puedan ver el resultado desenfocado.
Para ello, proporcionaremos el URI de la imagen de la magdalena que se muestra como entrada para nuestro WorkRequest
mostrado y, luego, usaremos la salida de la WorkRequest para mostrar imagen borrosa final.
Paso 1: Crea un objeto de entrada de datos
La entrada y el resultado se pasan en un sentido y otro por medio de objetos Data
. Los objetos Data
son contenedores livianos para pares clave-valor. Tienen el propósito de almacenar una pequeña cantidad de datos que podrían pasar desde WorkRequest
s y hacia ellas.
Pasarás el URI de la imagen del usuario a un paquete. Ese URI se almacenará en una variable llamada imageUri
.
En BlurViewModel
, crea un método privado llamado createInputDataForUri
. Con este método, harás lo siguiente:
- Crea un objeto
Data.Builder
. Importaandroidx.work.Data
cuando se te solicite hacerlo. - Si
imageUri
es unURI
no nulo, lo agregarás al objetoData
mediante el métodoputString
. Este método toma una clave y un valor. Puedes usar la constanteKEY_IMAGE_URI
de string de la claseConstants
. - Llama
build()
en el objetoData.Builder
a fin de crear tu objetoData
y mostrarlo.
A continuación, se muestra el método createInputDataForUri
completo:
BlurViewModel.kt
/**
* Creates the input data bundle which includes the Uri to operate on
* @return Data which contains the Image Uri as a String
*/
private fun createInputDataForUri(): Data {
val builder = Data.Builder()
imageUri?.let {
builder.putString(KEY_IMAGE_URI, imageUri.toString())
}
return builder.build()
}
Paso 2: Pasa el objeto de datos a WorkRequest
Cambiarás el método applyBlur
en BlurViewModel
de modo que realice lo siguiente:
- Cree una
OneTimeWorkRequestBuilder
nueva. - Llame a
setInputData
y pase el resultado decreateInputDataForUri
- Compile el
OneTimeWorkRequest
. - Ponga en cola la solicitud de trabajo mediante la solicitud
WorkManager
a fin de que el trabajo se programe para ejecutarse.
A continuación, se muestra el método applyBlur
completo:
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
.setInputData(createInputDataForUri())
.build()
workManager.enqueue(blurRequest)
}
Paso 3: Actualiza el método doWork() de BlurWorker para obtener la entrada
Actualicemos el método doWork()
de BlurWorker
a fin de obtener el URI que pasamos del objeto Data
:
BlurWorker.kt
override fun doWork(): Result {
val appContext = applicationContext
// ADD THIS LINE
val resourceUri = inputData.getString(KEY_IMAGE_URI)
// ... rest of doWork()
}
Paso 4: Difumina el URI dado
Con el URI, desenfocaremos la imagen de la magdalena en la pantalla.
- Quita el código anterior que tenía el recurso de imagen.
val picture = BitmapFactory.decodeResource(appContext.
resources
, R.drawable.
android_cupcake
)
- Verifica que no esté vacío el
resourceUri
obtenido deData
que se pasó. - Asigna la variable
picture
para que sea la imagen que se pasó de la siguiente manera:
val picture = BitmapFactory.decodeStream(
appContext.
contentResolver
.
`openInputStream(Uri.parse(resourceUri)))`
BlurWorker.kt
override fun doWork(): Result {
val appContext = applicationContext
val resourceUri = inputData.getString(KEY_IMAGE_URI)
makeStatusNotification("Blurring image", appContext)
return try {
// REMOVE THIS
// val picture = BitmapFactory.decodeResource(
// appContext.resources,
// R.drawable.android_cupcake)
if (TextUtils.isEmpty(resourceUri)) {
Log.e(TAG, "Invalid input uri")
throw IllegalArgumentException("Invalid input uri")
}
val resolver = appContext.contentResolver
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
val output = blurBitmap(picture, appContext)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(appContext, output)
Result.success()
} catch (throwable: Throwable) {
Log.e(TAG, "Error applying blur")
throwable.printStackTrace()
Result.failure()
}
}
Paso 5: URI temporal resultante
Ahora que terminaste con este trabajador, puedes mostrar el URI resultante en Result.success()
. Proporciona el URI de salida como datos de salida para facilitar el acceso a esta imagen temporal a otros trabajadores a fin de que realicen otras operaciones. Esto será útil en el siguiente capítulo, en el que crearás una cadena de trabajadores. Para ello, sigue estos pasos:
- Crea un nuevo objeto
Data
, tal como lo hiciste con la entrada, y almacenaoutputUri
como unaString
. Usa la misma clave,KEY_IMAGE_URI
. - Muestra esto a WorkManager mediante el método
Result.success(Data outputData)
.
BlurWorker.kt
Modifica la línea Result.success()
en doWork()
de la siguiente manera:
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
Paso 6: Ejecuta tu app
En este punto, debes ejecutar tu app. Debería compilarse y tener el mismo comportamiento en el que puedes ver la imagen difuminada a través del explorador de archivos de dispositivos, pero aún no en la pantalla.
Para buscar otra imagen desenfocada, puedes abrir el Explorador de archivos de dispositivos en Android Studio y navegar hasta data/data/com.example.background/files/blur_filter_outputs/<URI> tal como lo hiciste en el paso anterior.
Ten en cuenta que quizás debas realizar una sincronización para ver tus imágenes:
¡Muy bien! Desenfocaste una imagen de entrada usando WorkManager
.
6. Encadena tu trabajo
De momento, estás realizando una sola tarea: desenfocar la imagen. Este es un excelente primer paso, pero carece de algunas funciones principales:
- No limpia los archivos temporales.
- En realidad, no guarda la imagen en un archivo permanente.
- Siempre desenfoca la imagen de la misma manera.
Para agregar estas funciones, usaremos una cadena de trabajos de WorkManager.
WorkManager te permite crear WorkerRequest
independientes que se ejecutan en orden, o bien en paralelo. En este paso, crearás una cadena de trabajo que tiene el siguiente aspecto:
Las WorkRequest
se representan como cuadros.
Otra característica muy interesante del encadenamiento es que el resultado de una WorkRequest
se convierte en la entrada de la próxima WorkRequest
de la cadena. La entrada y el resultado que se pasan entre cada WorkRequest
se muestran como texto azul.
Paso 1: Crea trabajadores de limpieza y almacenamiento
Primero, deberás definir todas las clases Worker
que necesites. Ya tienes un Worker
para desenfocar una imagen, pero también necesitarás un Worker
que limpie los archivos temporales y un Worker
que guarde la imagen de forma permanente.
Crea dos clases nuevas en el paquete workers
que extiende Worker
.
El primero debe llamarse CleanupWorker
y, el segundo, SaveImageToFileWorker
.
Paso 2: Haz que extienda Worker
Extiende la clase CleanupWorker
de la clase Worker
. Agrega los parámetros de constructor requeridos.
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}
Paso 3: Anula e implementa doWork() para CleanupWorker
CleanupWorker
no necesita tomar ninguna entrada ni pasar ningún resultado. Siempre borrará los archivos temporales que existan. Como la manipulación de archivos está fuera del alcance de este codelab, puedes copiar el código del CleanupWorker
a continuación:
CleanupWorker.kt
package com.example.background.workers
import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
/**
* Cleans up temporary files generated during blurring process
*/
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
// Makes a notification when the work starts and slows down the work so that
// it's easier to see each WorkRequest start, even on emulated devices
makeStatusNotification("Cleaning up old temporary files", applicationContext)
sleep()
return try {
val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
if (outputDirectory.exists()) {
val entries = outputDirectory.listFiles()
if (entries != null) {
for (entry in entries) {
val name = entry.name
if (name.isNotEmpty() && name.endsWith(".png")) {
val deleted = entry.delete()
Log.i(TAG, "Deleted $name - $deleted")
}
}
}
}
Result.success()
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
}
}
}
Paso 4: Anula e implementa doWork() para SaveImageToFileWorker
SaveImageToFileWorker
admitirá entradas y resultados. La entrada es una String
del URI de la imagen difuminada temporalmente y almacenada con la clave KEY_IMAGE_URI
. El resultado también será una String
, el URI de la imagen difuminada guardada que se almacenó con la clave KEY_IMAGE_URI
.
Dado que este no es un codelab sobre la manipulación de archivos, a continuación se proporciona el código. Observa cómo se recuperan los valores resourceUri
y output
con la clave KEY_IMAGE_URI
. Esto es muy similar al código que escribiste en el paso anterior para la entrada y el resultado (utiliza las mismas claves).
SaveImageToFileWorker.kt
package com.example.background.workers
import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* Saves the image to a permanent file
*/
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
private val title = "Blurred Image"
private val dateFormatter = SimpleDateFormat(
"yyyy.MM.dd 'at' HH:mm:ss z",
Locale.getDefault()
)
override fun doWork(): Result {
// Makes a notification when the work starts and slows down the work so that
// it's easier to see each WorkRequest start, even on emulated devices
makeStatusNotification("Saving image", applicationContext)
sleep()
val resolver = applicationContext.contentResolver
return try {
val resourceUri = inputData.getString(KEY_IMAGE_URI)
val bitmap = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
val imageUrl = MediaStore.Images.Media.insertImage(
resolver, bitmap, title, dateFormatter.format(Date()))
if (!imageUrl.isNullOrEmpty()) {
val output = workDataOf(KEY_IMAGE_URI to imageUrl)
Result.success(output)
} else {
Log.e(TAG, "Writing to MediaStore failed")
Result.failure()
}
} catch (exception: Exception) {
exception.printStackTrace()
Result.failure()
}
}
}
Paso 5: Modifica la notificación de BlurWorker
Ahora que tenemos una cadena de Worker
que se encarga de guardar las imágenes en la carpeta correcta, podemos ralentizar el trabajo mediante el método sleep()
definido en la clase WorkerUtils
para que sea más fácil ver cada inicio de WorkRequest
, incluso en dispositivos emulados. La versión final de BlurWorker
será la siguiente:
BlurWorker.kt
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
override fun doWork(): Result {
val appContext = applicationContext
val resourceUri = inputData.getString(KEY_IMAGE_URI)
makeStatusNotification("Blurring image", appContext)
// ADD THIS TO SLOW DOWN THE WORKER
sleep()
// ^^^^
return try {
if (TextUtils.isEmpty(resourceUri)) {
Timber.e("Invalid input uri")
throw IllegalArgumentException("Invalid input uri")
}
val resolver = appContext.contentResolver
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)))
val output = blurBitmap(picture, appContext)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(appContext, output)
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
} catch (throwable: Throwable) {
throwable.printStackTrace()
Result.failure()
}
}
Paso 6: Crea una cadena de WorkRequest
Debes modificar el método applyBlur
de BlurViewModel
a fin de ejecutar una cadena de WorkRequest
en lugar de ejecutar solo una. Actualmente, el código tiene el siguiente aspecto:
BlurViewModel.kt
val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
.setInputData(createInputDataForUri())
.build()
workManager.enqueue(blurRequest)
En lugar de llamar a workManager.enqueue()
, llama a workManager.beginWith()
. Esto mostrará un WorkContinuation
, que define una cadena de WorkRequest
. Puedes agregar a esta cadena de solicitudes de trabajo llamando al método then()
, por ejemplo, si tienes tres objetos WorkRequest
, workA
, workB
y workC
, podrás hacer lo siguiente:
// Example code, don't copy to the project
val continuation = workManager.beginWith(workA)
continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
.then(workC)
.enqueue() // Enqueues the WorkContinuation which is a chain of work
Esto produciría y ejecutaría la siguiente cadena de WorkRequests:
Crea una cadena de WorkRequest
de CleanupWorker
, WorkRequest
de BlurImage
y WorkRequest
de SaveImageToFile
en applyBlur
. Pasa la entrada a la WorkRequest
de BlurImage
.
El código correspondiente se muestra a continuación:
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
// Add WorkRequest to Cleanup temporary images
var continuation = workManager
.beginWith(OneTimeWorkRequest
.from(CleanupWorker::class.java))
// Add WorkRequest to blur the image
val blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)
.setInputData(createInputDataForUri())
.build()
continuation = continuation.then(blurRequest)
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequest.Builder(SaveImageToFileWorker::class.java).build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
}
Esto debería compilar y ejecutar. Ahora, deberías poder presionar el botón Go y ver las notificaciones cuando se ejecuten los diferentes trabajadores. Aún podrás ver la imagen difuminada en el explorador de archivos de dispositivos y, en un paso siguiente, agregarás un botón adicional para que los usuarios puedan verla en el dispositivo.
En las siguientes capturas de pantalla, notarás que los mensajes de notificación muestran qué trabajador se está ejecutando actualmente.
Paso 7: Repite el BlurWorker
Es hora de agregar la capacidad de desenfocar una imagen varias veces. Toma el parámetro blurLevel
que pasaste a applyBlur
y agrega esa cantidad de operaciones WorkRequest
de desenfoque a la cadena. Solo la primera WorkRequest
necesitará contar con la entrada de URI.
Pruébalo y, luego, compáralo con el siguiente código:
BlurViewModel.kt
internal fun applyBlur(blurLevel: Int) {
// Add WorkRequest to Cleanup temporary images
var continuation = workManager
.beginWith(OneTimeWorkRequest
.from(CleanupWorker::class.java))
// Add WorkRequests to blur the image the number of times requested
for (i in 0 until blurLevel) {
val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
// Input the Uri if this is the first blur operation
// After the first blur operation the input will be the output of previous
// blur operations.
if (i == 0) {
blurBuilder.setInputData(createInputDataForUri())
}
continuation = continuation.then(blurBuilder.build())
}
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
}
Para ver las imágenes difuminadas, abre el explorador de archivos de dispositivos. Ten en cuenta que la carpeta de salida contiene varias imágenes desenfocadas, imágenes que se encuentran en las etapas intermedias de esta acción y la imagen final que muestra la imagen desenfocada en función de la cantidad de desenfoque que seleccionaste.
¡Buen “trabajo”! Ahora podrás desenfocar la imagen tanto como quieras. ¡Qué misterioso!
7. Garantiza el trabajo único
Ahora que usaste las cadenas, es hora de abordar otra poderosa función de WorkManager: las cadenas de trabajo único.
A veces, querrás que solo una cadena de trabajo se ejecute a la vez. Por ejemplo, tal vez tengas una cadena de trabajo que sincroniza tus datos locales con el servidor. Sería bueno permitir que la primera sincronización de datos termine antes de comenzar una nueva. Para hacerlo, deberás usar beginUniqueWork
en lugar de beginWith
y proporcionarle un nombre de String
único. Esto nombrará la cadena completa de solicitudes de trabajo a fin de que puedas hacer consultas y búsquedas en todas ellas.
Asegúrate de que la cadena de trabajo que desenfocará tu archivo sea única por medio de beginUniqueWork
. Pasa IMAGE_MANIPULATION_WORK_NAME
como la clave. También deberás pasar una ExistingWorkPolicy
. Tus opciones son REPLACE
, KEEP
o APPEND
.
Deberás usar REPLACE
porque, si el usuario decide desenfocar otra imagen antes de que se termine la actual, querremos detener la tarea actual y comenzar a desenfocar la imagen nueva.
El código para iniciar la continuación del trabajo único es el siguiente:
BlurViewModel.kt
// REPLACE THIS CODE:
// var continuation = workManager
// .beginWith(OneTimeWorkRequest
// .from(CleanupWorker::class.java))
// WITH
var continuation = workManager
.beginUniqueWork(
IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker::class.java)
)
Blur-O-Matic ahora solo desenfocará una imagen por vez.
8. Etiqueta y muestra el estado del trabajo
En esta sección, se usará LiveData de manera considerable, por lo que, a fin de que aproveches por completo lo que sigue, deberías estar familiarizado con LiveData. LiveData es un contenedor de datos observable optimizado para ciclos de vida.
Puedes consultar la documentación o el Codelab de componentes optimizados para ciclos de vida de Android si es la primera vez que trabajas con LiveData o clases observables.
El siguiente cambio importante que harás será cambiar lo que se muestra en la app a medida que se ejecuta el trabajo.
Puedes obtener el estado de cualquier WorkRequest
si obtienes un LiveData
que contiene un objeto WorkInfo
. WorkInfo
es un objeto que contiene detalles sobre el estado actual de una WorkRequest
, incluido lo siguiente:
- Si el trabajo fue
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
oSUCCEEDED
. - Si se completó
WorkRequest
, los datos de salida del trabajo
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 |
| Cada |
Obtener trabajo con un nombre de cadena único |
| Como acabas de ver, las |
Obtener trabajo con una etiqueta |
| Por último, también puedes etiquetar cualquier WorkRequest por medio de una String. Puedes etiquetar varias |
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
:
- En
BlurViewModel
, declara una nueva variable de clase llamadaoutputWorkInfos
, que es un elementoLiveData<List<WorkInfo>>
. - En
BlurViewModel
, agrega un bloque init a fin de obtener laWorkInfo
conWorkManager.getWorkInfosByTagLiveData
.
El código que necesitas es el siguiente:
BlurViewModel.kt
// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>
// Modify the existing init block in the BlurViewModel class to this:
init {
imageUri = getImageUri(application.applicationContext)
// This transformation makes sure that whenever the current work Id changes the WorkInfo
// the UI is listening to changes
outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
}
Paso 3: Muestra la WorkInfo
Ahora que tienes un LiveData
para tu WorkInfo
, puedes observarla en BlurActivity
. En el observador, haz lo siguiente:
- Verifica si la lista de
WorkInfo
no es nula y si tiene objetosWorkInfo
en ella. De no ser así, entonces aún no se hizo clic en el botón Go. En ese caso, vuelve atrás. - Obtén las primeras
WorkInfo
de la lista; solo habrá unaWorkInfo
etiquetada conTAG_OUTPUT
, ya que hicimos que la cadena de trabajo fuera única. - Comprueba si finalizó el estado del trabajo mediante
workInfo.state.isFinished
. - Si no está terminado, llama a
showWorkInProgress()
, que oculta el botón Go y muestra el botón Cancel Work y la barra de progreso. - Si se completó el proceso, llama a
showWorkFinished()
, que ocultará el botón Cancel Work y la barra de progreso, y mostrará el botón Go.
Este es el código:
Nota: Importa androidx.lifecycle.Observer
cuando se te solicite hacerlo.
BlurActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
...
// Observe work status, added in onCreate()
viewModel.outputWorkInfos.observe(this, workInfosObserver())
}
// Define the observer function
private fun workInfosObserver(): Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
// Note that these next few lines grab a single WorkInfo if it exists
// This code could be in a Transformation in the ViewModel; they are included here
// so that the entire process of displaying a WorkInfo is in one location.
// If there are no matching work info, do nothing
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
// We only care about the one output status.
// Every continuation has only one worker tagged TAG_OUTPUT
val workInfo = listOfWorkInfo[0]
if (workInfo.state.isFinished) {
showWorkFinished()
} else {
showWorkInProgress()
}
}
}
Paso 4: Ejecuta tu app
Ejecuta tu app. Debería compilarse y ejecutarse, y ahora mostrará una barra de progreso cuando esté funcionando, así como el botón Cancelar:
9. Muestra el resultado final
Cada WorkInfo
también tiene un método getOutputData
que te permitirá obtener el objeto Data
resultante junto con la imagen final guardada. En Kotlin, puedes acceder a este método usando una variable que el lenguaje genera para ti: outputData
. Mostremos un botón que diga See File cuando haya una imagen desenfocada lista para mostrarse.
Paso 1: Crea el botón See File
Ya hay un botón en el diseño de activity_blur.xml
que está oculto. Se encuentra en BlurActivity
y se llama outputButton
.
En BlurActivity
, dentro de onCreate()
, configura el objeto de escucha de clics para ese botón. Deberías obtener el URI y, luego, abrir una actividad a fin de verlo. Puedes usar el siguiente código:
BlurActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
// Setup view output image file button
binding.seeFileButton.setOnClickListener {
viewModel.outputUri?.let { currentUri ->
val actionView = Intent(Intent.ACTION_VIEW, currentUri)
actionView.resolveActivity(packageManager)?.run {
startActivity(actionView)
}
}
}
}
Paso 2: Establece el URI y muestra el botón
Para hacer esto, deberás aplicar algunos ajustes finales al observador de WorkInfo
:
- Si
WorkInfo
finalizó, obtén los datos resultantes por medio deworkInfo.outputData
. - Luego, obtén el URI resultante. Recuerda que se almacena con la clave
Constants.KEY_IMAGE_URI
. - Luego, si el URI no está vacío, se guardará correctamente. Muestra el
outputButton
y llama asetOutputUri
en el modelo de vista con el URI.
BlurActivity.kt
private fun workInfosObserver(): Observer<List<WorkInfo>> {
return Observer { listOfWorkInfo ->
// Note that these next few lines grab a single WorkInfo if it exists
// This code could be in a Transformation in the ViewModel; they are included here
// so that the entire process of displaying a WorkInfo is in one location.
// If there are no matching work info, do nothing
if (listOfWorkInfo.isNullOrEmpty()) {
return@Observer
}
// We only care about the one output status.
// Every continuation has only one worker tagged TAG_OUTPUT
val workInfo = listOfWorkInfo[0]
if (workInfo.state.isFinished) {
showWorkFinished()
// Normally this processing, which is not directly related to drawing views on
// screen would be in the ViewModel. For simplicity we are keeping it here.
val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)
// If there is an output file show "See File" button
if (!outputImageUri.isNullOrEmpty()) {
viewModel.setOutputUri(outputImageUri)
binding.seeFileButton.visibility = View.VISIBLE
}
} else {
showWorkInProgress()
}
}
}
Paso 3: Ejecuta el código
Ejecuta tu código. Deberías ver el nuevo botón See File en el que se puede hacer clic, que te llevará al archivo resultante:
10. Cancela el trabajo
Agregaste este botón Cancel Work. Ahora agreguemos el código para que haga algo. Con WorkManager, puedes cancelar trabajos usando el ID, por etiqueta y por nombre de cadena único.
En este caso, querrás cancelar el trabajo por nombre de cadena único, ya que quieres cancelar todo el trabajo de la cadena, no solo un paso en particular.
Paso 1: Cancela el trabajo por nombre
En BlurViewModel
, agrega un método nuevo llamado cancelWork()
para cancelar el trabajo único. Dentro de la función, llama a cancelUniqueWork
en el workManager
y pasa la etiqueta IMAGE_MANIPULATION_WORK_NAME
.
BlurViewModel.kt
internal fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
Paso 2: Llama al método de cancelación
Luego, conecta el botón cancelButton
para que llame a cancelWork
:
BlurActivity.kt
// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }
Paso 3: Ejecuta y cancela tu trabajo
Ejecuta la app. Debería compilarse bien. Empieza a desenfocar una imagen y, luego, haz clic en el botón Cancelar. Se cancelará toda la cadena.
Ten en cuenta que solo se muestra el botón GO una vez que se cancela el trabajo porque WorkState ya no está en estado FINISHED.
11. Restricciones de trabajos
Por último, WorkManager
admite Constraints
. En el caso de Blur-O-Matic, usarás la restricción que establece que el dispositivo deberá estar cargándose. Esto significa que tu solicitud de trabajo solo se ejecutará si el dispositivo se está cargando.
Paso 1: Crea y agrega la restricción de carga
Para crear un objeto Constraints
, deberás usar un Constraints.Builder
. Luego, deberás configurar las restricciones que desees y agregarlas a la WorkRequest
con el método setRequiresCharging()
como se muestra a continuación:
Importa androidx.work.Constraints
cuando se te solicite hacerlo.
BlurViewModel.kt
// Put this inside the applyBlur() function, above the save work request.
// Create charging constraint
val constraints = Constraints.Builder()
.setRequiresCharging(true)
.build()
// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.setConstraints(constraints)
.addTag(TAG_OUTPUT)
.build()
continuation = continuation.then(save)
// Actually start the work
continuation.enqueue()
Paso 2: Realiza las pruebas con el emulador o el dispositivo
Ya puedes ejecutar Blur-O-Matic. Si estás usando un dispositivo, puedes quitarlo o conectarlo. En un emulador, puedes cambiar el estado de carga en la ventana Extended controls:
Cuando el dispositivo esté desconectado, deberá suspender SaveImageToFileWorker,
y ejecutarlo solo después de que lo conectes.
12. Felicitaciones
Felicitaciones. Terminaste la app de Blur-O-Matic y, en el proceso, aprendiste lo siguiente:
- Cómo agregar WorkManager a tu Proyecto
- Cómo programar un
OneTimeWorkRequest
- Parámetros de entrada y salida
- Cómo encadenar el trabajo con
WorkRequest
- Cómo asignar nombres a cadenas de
WorkRequest
únicas - Cómo etiquetar
WorkRequest
- Cómo mostrar
WorkInfo
en la IU - Cómo cancelar
WorkRequest
- Cómo agregar restricciones a una
WorkRequest
¡Buen "trabajo"! Para ver el estado final del código y todos los cambios, consulta lo siguiente:
También puedes clonar el codelab de WorkManager desde GitHub:
$ git clone https://github.com/googlecodelabs/android-workmanager
WorkManager admite mucho más de lo que podríamos abarcar en este codelab, incluido el trabajo repetitivo, una biblioteca de compatibilidad de pruebas, solicitudes de trabajo paralelas y combinaciones de entrada. Para obtener más información, consulta la documentación de WorkManager o continúa con el codelab avanzado WorkManager.