En Android, hay muchas opciones para realizar trabajos diferibles en segundo plano. Este codelab trata sobre WorkManager, una biblioteca compatible, flexible y simple a fin de realizar trabajos diferibles en segundo plano. WorkManager es el programador de tareas recomendado de Android para realizar trabajos diferibles, que garantiza su ejecución.
¿Qué es WorkManager?
WorkManager es parte de Android Jetpack y un componente de la arquitectura para trabajos en segundo plano que requieren una ejecución tanto oportunista como garantizada. La ejecución oportunista implica que WorkManager realizará el trabajo en segundo plano tan pronto como sea posible. La ejecución garantizada implica que WorkManager se encargará de la lógica a los efectos de iniciar tu trabajo en diferentes situaciones, incluso si sales de la app.
WorkManager es una biblioteca sencilla, pero extremadamente flexible, con muchos beneficios adicionales. Por ejemplo:
- Es compatible con tareas asíncronas únicas y periódicas.
- Admite restricciones, como condiciones de red, espacio de almacenamiento y estado de carga.
- Encadena solicitudes de trabajo complejas, incluido el trabajo de ejecución en paralelo.
- Utiliza el resultado de una solicitud de trabajo como entrada para la siguiente.
- Controla la compatibilidad con el nivel de API 14 (consulta la nota).
- Funciona con o sin los Servicios de Google Play.
- Sigue las recomendaciones para proteger el sistema.
- Ofrece compatibilidad con LiveData a fin de mostrar fácilmente el estado de la solicitud de trabajo en la IU.
Cuándo usar WorkManager
La biblioteca de WorkManager es una buena opción para las tareas que resultan útiles de completar, incluso si el usuario sale de una pantalla en particular o de tu app.
Algunos ejemplos de tareas que muestran un buen uso de WorkManager:
- Subir registros
- Aplicar filtros a imágenes y guardar la imagen
- Sincronizar datos locales con la red de forma periódica
WorkManager ofrece una ejecución garantizada, y no todas las tareas lo necesitan. Por consiguiente, su función no es la de ejecutar todas las tareas del subproceso principal. Si quieres obtener más información sobre el uso de WorkManager, consulta la Guía para el procesamiento en segundo plano.
Qué compilarás
Hoy en día, los smartphones son muy buenos para tomar fotos. Atrás quedaron los días en que un fotógrafo tomara con seguridad una foto desenfocada de algo misterioso.
En este codelab, trabajarás en Blur-O-Matic, una app que desenfoca fotos e imágenes, y guarda el resultado en un archivo. ¿Eso era el monstruo del Lago Ness o un submarino de juguete? Con Blur-O-Matic, nadie lo sabrá jamás.
Foto de lubina estriada híbrida, tomada por Peggy Greb, Servicio de Investigación Agrícola del Departamento de Agricultura de los Estados Unidos. |
Qué aprenderás
- Cómo agregar WorkManager a tu proyecto
- Cómo programar una tarea simple
- Parámetros de entrada y salida
- Trabajos en cadena
- Trabajo único
- Cómo mostrar el estado de trabajo en la IU
- Cómo cancelar trabajos
- Restricciones de trabajos
Requisitos
- 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 una View, una introducción a los componentes de la arquitectura)
Si en algún momento no puedes avanzar…
Si en algún momento no puedes avanzar con este codelab o si deseas ver el estado final del código, puedes usar el siguiente vínculo:
Como alternativa, puedes clonar el codelab completo de WorkManager desde GitHub:
$ git clone -b java https://github.com/googlecodelabs/android-workmanager
Paso 1: Descarga el código
Haz clic en el siguiente vínculo a fin de descargar todo el código de este codelab:
Si lo prefieres, también puedes clonar el codelab de navegación desde GitHub:
$ git clone -b start_java https://github.com/googlecodelabs/android-workmanager
Paso 2: Obtén una imagen
Si usas un dispositivo en el que ya descargaste o tomaste fotos, ya puedes comenzar.
Si usas un dispositivo nuevo (como un emulador que se creó en forma reciente), te recomendamos que tomes una foto o descargues una imagen de la Web con el dispositivo. ¡Elige algo misterioso!
Paso 3: Ejecuta la app
Ejecuta la app. Deberías ver las siguientes pantallas (asegúrate de habilitar los permisos de acceso a las fotos a partir de la solicitud inicial y, si la imagen está inhabilitada, vuelve a abrir la app):
Puedes seleccionar una imagen e ir a la pantalla siguiente, que tiene botones de selección con los que podrás seleccionar cuánto desenfocar tu imagen. Si presionas el botón Go, la imagen se desenfocará y se guardará.
A partir de este momento, la app dejará de desenfocar la imagen.
El código inicial contiene lo siguiente:
WorkerUtils
**:** Esta clase contiene el código de la acción de desenfoque y algunos métodos útiles que usarás más tarde para mostrarNotifications
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.SelectImageActivity
**:** Esta es la primera actividad que te permitirá seleccionar una imagen.res/activity_blur.xml
yres/activity_select.xml
: Estos son los archivos de diseño de cada actividad.
***** Estos son los únicos archivos en los que escribirás código.
WorkManager
requiere la dependencia de Gradle que se indica a continuación. Estas ya se incluyeron en los archivos de compilación:
app/build.gradle
dependencies {
// Other dependencies
implementation "androidx.work:work-runtime:$versions.work"
}
Deberás obtener la versión más reciente de work-runtime
aquí y colocar la versión correcta. En este momento, la versión más reciente es:
build.gradle
versions.work = "2.3.3"
Si actualizas tu versión a una más reciente, asegúrate de usar la opción Sincronizar ahora a fin de sincronizar tu proyecto con los archivos de Gradle modificados.
En este paso, tomarás una imagen de la carpeta res/drawable
llamada test.jpg
y ejecutarás algunas funciones sobre ella en segundo plano. Estas funciones desenfocarán la imagen y la guardarán en un archivo temporal.
Aspectos básicos de WorkManager
Existen algunas clases de WorkManager que debes conocer:
Worker
: Aquí es donde colocas el código del trabajo real que deseas realizar en segundo plano. Extenderás esta clase y anularás el mé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
de manera que se distribuya la carga sobre los recursos del sistema, respetando las restricciones que hayas especificado.
En tu caso, definirás un nuevo BlurWorker
que contendrá el código para desenfocar una imagen. Cuando se haga clic en el botón Go, se creará una WorkRequest
y, luego, WorkManager
lo pondrá en cola.
Paso 1: Crea el BlurWorker
En el paquete workers
, crea una nueva clase llamada BlurWorker
.
Debería extender Worker
.
Paso 2: Agrega un constructor
Agrega un constructor a la clase BlurWorker
:
public class BlurWorker extends Worker {
public BlurWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
}
Paso 3: Anula e implementa doWork()
Tu Worker
desenfocará la imagen res/test.jpg
.
Anula el método doWork()
y, luego, implementa lo siguiente:
- Obtén un
Context
llamando agetApplicationContext()
. Lo necesitarás para las diferentes manipulaciones de mapas de bits que estás por hacer. - Crea un
Bitmap
a partir de la imagen de prueba:
Bitmap picture = BitmapFactory.decodeResource(
applicationContext.getResources(),
R.drawable.test);
- Obtén una versión desenfocada del mapa de bits llamando al método estático
blurBitmap
desdeWorkerUtils
. - Escribe ese mapa de bits en un archivo temporal llamando al método estático
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 estático
makeStatusNotification
desdeWorkerUtils
. - Muestra
Result.success();
. - Une el código de los pasos 2 a 6 en una sentencia try/catch. Captura un elemento
Throwable
genérico. - En la sentencia de captura, emite una instrucción de registro de error:
Log.e(TAG, "Error applying blur", throwable);
. - Luego, en la sentencia de captura, muestra
Result.failure();
.
A continuación, se incluye el código completo correspondiente a este paso.
BlurWorker.java
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;
import com.example.background.R;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
public class BlurWorker extends Worker {
public BlurWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
private static final String TAG = BlurWorker.class.getSimpleName();
@NonNull
@Override
public Result doWork() {
Context applicationContext = getApplicationContext();
try {
Bitmap picture = BitmapFactory.decodeResource(
applicationContext.getResources(),
R.drawable.test);
// Blur the bitmap
Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);
// Write bitmap to a temp file
Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);
WorkerUtils.makeStatusNotification("Output is "
+ outputUri.toString(), applicationContext);
// If there were no errors, return SUCCESS
return Result.success();
} catch (Throwable throwable) {
// Technically WorkManager will return Result.failure()
// but it's best to be explicit about it.
// Thus if there were errors, we're return FAILURE
Log.e(TAG, "Error applying blur", throwable);
return Result.failure();
}
}
}
Paso 4: Obtén WorkManager en el ViewModel
Crea una variable para una instancia WorkManager
en tu ViewModel
y crea una instancia de ella en el constructor de ViewModel
:
BlurViewModel.java
private WorkManager mWorkManager;
// BlurViewModel constructor
public BlurViewModel(@NonNull Application application) {
super(application);
mWorkManager = WorkManager.getInstance(application);
//...rest of the constructor
}
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 applyBlur() de BlurViewModel
:
BlurViewModel.java
void applyBlur(int blurLevel) {
mWorkManager.enqueue(OneTimeWorkRequest.from(BlurWorker.class));
}
Paso 6: Ejecuta el código
Ejecuta tu código. Debería compilarse, y tú deberías ver la notificación cuando presiones el botón Go.
De manera opcional, puedes abrir el Explorador de archivos de dispositivos en Android Studio:
Luego, navega a data>data>com.example.background>files>blur_filter_outputs><URI> y confirma que el pez esté efectivamente desenfocado:
Desenfocar esa imagen de prueba está muy bien, pero para que Blur-O-Matic en verdad sea una app revolucionaria de edición de imágenes, deberás permitir que los usuarios desenfoquen sus propias imágenes.
Para hacerlo, proporcionaremos el URI de la imagen seleccionada del usuario como entrada en nuestro WorkRequest
.
Paso 1: Crea el objeto de entrada de datos
La entrada y el resultado se pasan en un sentido y otro por medio de objetos Data
. Los objetos Data
son contenedores livianos para pares clave-valor. Tienen el propósito de almacenar una pequeña cantidad de datos que podrían pasar desde WorkRequest
y hacia ellas.
Pasarás el URI de la imagen del usuario a un paquete. Ese URI se almacenará en una variable llamada mImageUri
.
Crea un método privado llamado createInputDataForUri
. Con este método harás lo siguiente:
- Crearás un objeto
Data.Builder
. - Si
mImageUri
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 a
build()
en el objetoData.Builder
a fin de crear tu objetoData
y mostrarlo.
A continuación, se muestra el método createInputDataForUri
completo:
BlurViewModel.java
/**
* Creates the input data bundle which includes the Uri to operate on
* @return Data which contains the Image Uri as a String
*/
private Data createInputDataForUri() {
Data.Builder builder = new Data.Builder();
if (mImageUri != null) {
builder.putString(KEY_IMAGE_URI, mImageUri.toString());
}
return builder.build();
}
Paso 2: Pasa el objeto de datos a WorkRequest
Te recomendamos que cambies el método applyBlur
para que realice lo siguiente:
- Cree una
OneTimeWorkRequest.Builder
nueva - Llame a
setInputData
y pase el resultado decreateInputDataForUri
- Compile el
OneTimeWorkRequest
- Ponga en cola las solicitudes que usen
WorkManager
A continuación, se muestra el método applyBlur
completo:
BlurViewModel.java
void applyBlur(int blurLevel) {
OneTimeWorkRequest blurRequest =
new OneTimeWorkRequest.Builder(BlurWorker.class)
.setInputData(createInputDataForUri())
.build();
mWorkManager.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.java
public Result doWork() {
Context applicationContext = getApplicationContext();
// ADD THIS LINE
String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);
//... rest of doWork()
}
Esta variable no se usará hasta que completes los pasos siguientes.
Paso 4: Difumina el URI dado
Por medio del URI, puedes desenfocar la imagen que seleccionó el usuario:
BlurWorker.java
public Worker.Result doWork() {
Context applicationContext = getApplicationContext();
String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);
try {
// REPLACE THIS CODE:
// Bitmap picture = BitmapFactory.decodeResource(
// applicationContext.getResources(),
// R.drawable.test);
// WITH
if (TextUtils.isEmpty(resourceUri)) {
Log.e(TAG, "Invalid input uri");
throw new IllegalArgumentException("Invalid input uri");
}
ContentResolver resolver = applicationContext.getContentResolver();
// Create a bitmap
Bitmap picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)));
//...rest of doWork
Paso 5: URI temporal resultante
Como ya terminamos con este Trabajador, ahora podemos mostrar Result.success()
. Proporcionaremos el OutputURI como un dato de salida para facilitar el acceso a esta imagen temporal a otros trabajadores a fin de realizar otras operaciones. Esto será útil en el siguiente capítulo, en el que crearemos una cadena de trabajadores. Para hacer lo siguiente:
- Crea un nuevo objeto
Data
, tal como lo hiciste con la entrada, y almacenaoutputUri
como unaString
. Usa la misma clave,KEY_IMAGE_URI
. - Pasa esto al método
Result.success()
deWorker
.
BlurWorker.java
Esta línea deberá seguir a la línea WorkerUtils.makeStatusNotification
y reemplazar Result.success()
en doWork()
:
Data outputData = new Data.Builder()
.putString(KEY_IMAGE_URI, outputUri.toString())
.build();
return Result.success(outputData);
Paso 6: Ejecuta tu app
Ejecuta tu app. Debería compilar y tener el mismo comportamiento.
De manera opcional, puedes abrir el Explorador de archivos de dispositivos en Android Studio y navegar a data/data/com.example.background/files/blur_filter_outputs/<URI> tal como lo hiciste en el paso anterior.
Ten en cuenta que quizás debas realizar una sincronización para ver tus imágenes:
¡Muy bien! Desenfocaste una imagen de entrada usando WorkManager
.
De momento, estás realizando una sola tarea: desenfocar la imagen. Este es un excelente primer paso, pero carece de algunas funciones principales:
- No limpia los archivos temporales.
- En realidad, no guarda la imagen en un archivo permanente.
- Siempre desenfoca la imagen de la misma manera.
Para agregar estas funciones, usaremos una cadena de trabajos de WorkManager.
WorkManager te permite crear 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 worker
que extiende Worker
.
El primero debe llamarse CleanupWorker
, y el segundo, SaveImageToFileWorker
.
Paso 2: Agrega un constructor
Agrega un constructor a la clase CleanupWorker
:
public class CleanupWorker extends Worker {
public CleanupWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
}
Paso 3: Anula e implementa doWork() para CleanupWorker
CleanupWorker
no necesita tomar ninguna entrada ni pasar ningún resultado. Siempre borrará los archivos temporales que existan. Dado que este no es un codelab sobre la manipulación de archivos, puedes copiar el código de CleanupWorker
que aparece a continuación:
CleanupWorker.java
import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.io.File;
public class CleanupWorker extends Worker {
public CleanupWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
private static final String TAG = CleanupWorker.class.getSimpleName();
@NonNull
@Override
public Result doWork() {
Context applicationContext = getApplicationContext();
// 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
WorkerUtils.makeStatusNotification("Cleaning up old temporary files",
applicationContext);
WorkerUtils.sleep();
try {
File outputDirectory = new File(applicationContext.getFilesDir(),
Constants.OUTPUT_PATH);
if (outputDirectory.exists()) {
File[] entries = outputDirectory.listFiles();
if (entries != null && entries.length > 0) {
for (File entry : entries) {
String name = entry.getName();
if (!TextUtils.isEmpty(name) && name.endsWith(".png")) {
boolean deleted = entry.delete();
Log.i(TAG, String.format("Deleted %s - %s",
name, deleted));
}
}
}
}
return Worker.Result.success();
} catch (Exception exception) {
Log.e(TAG, "Error cleaning up", exception);
return Worker.Result.failure();
}
}
}
Paso 4: Anula e implementa doWork() para SaveImageToFileWorker
SaveImageToFileWorker
admitirá entradas y resultados. La entrada será una String
almacenada con la clave KEY_IMAGE_URI
. El resultado también será una String
almacenada con la clave KEY_IMAGE_URI
.
Dado que este no es un codelab sobre la manipulación de archivos, a continuación se incluye el código, con dos TODO
a fin de que los completes con el código apropiado para la entrada y el resultado. Esto es muy similar al código que escribiste en el paso anterior para la entrada y el resultado (utiliza las mismas claves).
SaveImageToFileWorker.java
import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
public class SaveImageToFileWorker extends Worker {
public SaveImageToFileWorker(
@NonNull Context appContext,
@NonNull WorkerParameters workerParams) {
super(appContext, workerParams);
}
private static final String TAG = SaveImageToFileWorker.class.getSimpleName();
private static final String TITLE = "Blurred Image";
private static final SimpleDateFormat DATE_FORMATTER =
new SimpleDateFormat("yyyy.MM.dd 'at' HH:mm:ss z", Locale.getDefault());
@NonNull
@Override
public Result doWork() {
Context applicationContext = getApplicationContext();
// 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
WorkerUtils.makeStatusNotification("Saving image", applicationContext);
WorkerUtils.sleep();
ContentResolver resolver = applicationContext.getContentResolver();
try {
String resourceUri = getInputData()
.getString(Constants.KEY_IMAGE_URI);
Bitmap bitmap = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)));
String outputUri = MediaStore.Images.Media.insertImage(
resolver, bitmap, TITLE, DATE_FORMATTER.format(new Date()));
if (TextUtils.isEmpty(outputUri)) {
Log.e(TAG, "Writing to MediaStore failed");
return Result.failure();
}
Data outputData = new Data.Builder()
.putString(Constants.KEY_IMAGE_URI, outputUri)
.build();
return Result.success(outputData);
} catch (Exception exception) {
Log.e(TAG, "Unable to save image to Gallery", exception);
return Worker.Result.failure();
}
}
}
Paso 5: Modifica la notificación de BlurWorker
Ahora que tenemos una cadena de Worker
que se encarga de guardar la imagen en la carpeta correcta, podemos modificar la notificación a fin de informar a los usuarios cuando el trabajo comience y se lo ralentice de modo que sea más fácil ver el inicio de cada WorkRequest
, incluso en dispositivos emulados. La versión final de BlurWorker
será la siguiente:
BlurWorker.java
@NonNull
@Override
public Worker.Result doWork() {
Context applicationContext = getApplicationContext();
// 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
WorkerUtils.makeStatusNotification("Blurring image", applicationContext);
WorkerUtils.sleep();
String resourceUri = getInputData().getString(KEY_IMAGE_URI);
try {
if (TextUtils.isEmpty(resourceUri)) {
Log.e(TAG, "Invalid input uri");
throw new IllegalArgumentException("Invalid input uri");
}
ContentResolver resolver = applicationContext.getContentResolver();
// Create a bitmap
Bitmap picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri)));
// Blur the bitmap
Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);
// Write bitmap to a temp file
Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);
Data outputData = new Data.Builder()
.putString(KEY_IMAGE_URI, outputUri.toString())
.build();
// If there were no errors, return SUCCESS
return Result.success(outputData);
} catch (Throwable throwable) {
// Technically WorkManager will return Result.failure()
// but it's best to be explicit about it.
// Thus if there were errors, we're return FAILURE
Log.e(TAG, "Error applying blur", throwable);
return 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.java
void applyBlur(int blurLevel) {
OneTimeWorkRequest blurRequest =
new OneTimeWorkRequest.Builder(BlurWorker.class)
.setInputData(createInputDataForUri())
.build();
mWorkManager.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
WorkContinuation continuation = mWorkManager.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.java
void applyBlur(int blurLevel) {
// Add WorkRequest to Cleanup temporary images
WorkContinuation continuation =
mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// Add WorkRequest to blur the image
OneTimeWorkRequest blurRequest = new OneTimeWorkRequest.Builder(BlurWorker.class)
.setInputData(createInputDataForUri())
.build();
continuation = continuation.then(blurRequest);
// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save =
new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.build();
continuation = continuation.then(save);
// Actually start the work
continuation.enqueue();
}
Esto debería compilar y ejecutar. Deberías poder ver la imagen que elegiste desenfocar guardada en la carpeta Pictures:
Paso 7: Repite el BlurWorker
Es hora de agregar la capacidad de desenfocar la imagen reiteradas veces. Toma el parámetro blurLevel
pasado a applyBlur
y agrega esa cantidad de operaciones WorkRequest
de desenfoque a la cadena. Solo la primera WorkRequest
necesitará contar con la entrada de URI.
Pruébalo y, luego, compáralo con el siguiente código:
BlurViewModel.java
void applyBlur(int blurLevel) {
// Add WorkRequest to Cleanup temporary images
WorkContinuation continuation = mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// Add WorkRequests to blur the image the number of times requested
for (int i = 0; i < blurLevel; i++) {
OneTimeWorkRequest.Builder blurBuilder =
new OneTimeWorkRequest.Builder(BlurWorker.class);
// 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
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.build();
continuation = continuation.then(save);
// Actually start the work
continuation.enqueue();
}
¡Buen "trabajo"! Ahora podrás desenfocar la imagen tanto como quieras. ¡Qué misterioso!
Ahora que usaste las cadenas, es hora de abordar otra poderosa función de WorkManager: las cadenas de trabajo único.
A veces, querrás que solo una cadena de trabajo se ejecute a la vez. Por ejemplo, tal vez tengas una cadena de trabajo que sincroniza tus datos locales con el servidor. Sería bueno permitir que la primera sincronización de datos termine antes de comenzar una nueva. Para hacerlo, deberás usar beginUniqueWork
en lugar de beginWith
y proporcionarle un nombre de String
único. Esto nombrará la cadena completa de solicitudes de trabajo a fin de que puedas hacer consultas y búsquedas en todas ellas.
Asegúrate de que la cadena de trabajo que desenfocará tu archivo sea única por medio de beginUniqueWork
. Pasa IMAGE_MANIPULATION_WORK_NAME
como la clave. También deberás pasar una ExistingWorkPolicy
. Tus opciones son REPLACE
, KEEP
o APPEND
.
Deberás usar REPLACE
porque, si el usuario decide desenfocar otra imagen antes de que se termine la actual, querremos detener la tarea actual y comenzar a desenfocar la imagen nueva.
El código para iniciar la continuación del trabajo único es el siguiente:
BlurViewModel.java
// REPLACE THIS CODE:
// WorkContinuation continuation =
// mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// WITH
WorkContinuation continuation = mWorkManager
.beginUniqueWork(IMAGE_MANIPULATION_WORK_NAME,
ExistingWorkPolicy.REPLACE,
OneTimeWorkRequest.from(CleanupWorker.class));
Blur-O-Matic ahora solo desenfocará una imagen por vez.
En esta sección, se usará LiveData de manera considerable, por lo que, a fin de que aproveches por completo lo que sigue, deberías estar familiarizado con LiveData. LiveData es un contenedor de datos observable optimizado para ciclos de vida.
Puedes consultar la documentación o el Codelab de componentes optimizados para ciclos de vida de Android si es la primera vez que trabajas con LiveData o clases observables.
El siguiente cambio importante que harás será cambiar lo que se muestra en la app a medida que se ejecuta el trabajo.
Puedes obtener el estado de cualquier WorkRequest
si obtienes un LiveData
que contiene un objeto WorkInfo
. WorkInfo
es un objeto que contiene detalles sobre el estado actual de una WorkRequest
, incluido lo siguiente:
- 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 getWorkInfosByTagLiveData
. 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 getWorkInfosForUniqueWorkLiveData
, 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.java
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.addTag(TAG_OUTPUT) // This adds the tag
.build();
Paso 2: Obtén la WorkInfo
Ahora que etiquetaste el trabajo, podrás obtener la WorkInfo
:
- Declara una nueva variable llamada
mSavedWorkInfo
, que es unLiveData<List<WorkInfo>>
. - En el constructor de
BlurViewModel
, obtén laWorkInfo
medianteWorkManager.getWorkInfosByTagLiveData
. - Agrega un método get para
mSavedWorkInfo
.
El código que necesitas es el siguiente:
BlurViewModel.java
// New instance variable for the WorkInfo class
private LiveData<List<WorkInfo>> mSavedWorkInfo;
// Placed this code in the BlurViewModel constructor
mSavedWorkInfo = mWorkManager.getWorkInfosByTagLiveData(TAG_OUTPUT);
// Add a getter method for mSavedWorkInfo
LiveData<List<WorkInfo>> getOutputWorkInfo() { return mSavedWorkInfo; }
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.getState().isFinished();
. - Si no está terminado, llama a
showWorkInProgress()
, que ocultará y mostrará las vistas adecuadas. - Cuando termine, llama al objeto
showWorkFinished()
, que ocultará y mostrará las vistas correspondientes.
Este es el código:
BlurActivity.java
// Show work status, added in onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfos -> {
// If there are no matching work info, do nothing
if (listOfWorkInfos == null || listOfWorkInfos.isEmpty()) {
return;
}
// We only care about the first output status.
// Every continuation has only one worker tagged TAG_OUTPUT
WorkInfo workInfo = listOfWorkInfos.get(0);
boolean finished = workInfo.getState().isFinished();
if (!finished) {
showWorkInProgress();
} else {
showWorkFinished();
}
});
Paso 4: Ejecuta tu app
Ejecuta tu app. Debería compilarse y ejecutarse, y ahora mostrará una barra de progreso cuando esté funcionando, así como el botón Cancelar:
Cada WorkInfo
también tiene un método getOutputData
que te permitirá obtener el objeto Data
resultante junto con la imagen final guardada. Mostremos un botón que diga See File cuando haya una imagen desenfocada lista para mostrarse.
Paso 1: Crea mOutputUri
Crea una variable en BlurViewModel
para el URI final y proporciona métodos get y set para ella. A fin de convertir un objeto String
en un Uri
, puedes usar el método uriOrNull
.
Puedes usar el siguiente código:
BlurViewModel.java
// New instance variable for the WorkInfo
private Uri mOutputUri;
// Add a getter and setter for mOutputUri
void setOutputUri(String outputImageUri) {
mOutputUri = uriOrNull(outputImageUri);
}
Uri getOutputUri() { return mOutputUri; }
Paso 2: 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 puede acceder a él a través de su vinculación de vista como seeFileButton
.
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.java
// Inside onCreate()
binding.seeFileButton.setOnClickListener(view -> {
Uri currentUri = mViewModel.getOutputUri();
if (currentUri != null) {
Intent actionView = new Intent(Intent.ACTION_VIEW, currentUri);
if (actionView.resolveActivity(getPackageManager()) != null) {
startActivity(actionView);
}
}
});
Paso 3: 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.getOutputData().
. - 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
seeFileButton
y llama asetOutputUri
en el modelo de vista con el URI.
BlurActivity.java
// Replace the observer code we added in previous steps with this one.
// Show work info, goes inside onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfo -> {
// If there are no matching work info, do nothing
if (listOfWorkInfo == null || listOfWorkInfo.isEmpty()) {
return;
}
// We only care about the first output status.
// Every continuation has only one worker tagged TAG_OUTPUT
WorkInfo workInfo = listOfWorkInfo.get(0);
boolean finished = workInfo.getState().isFinished();
if (!finished) {
showWorkInProgress();
} else {
showWorkFinished();
Data outputData = workInfo.getOutputData();
String outputImageUri = outputData.getString(Constants.KEY_IMAGE_URI);
// If there is an output file show "See File" button
if (!TextUtils.isEmpty(outputImageUri)) {
mViewModel.setOutputUri(outputImageUri);
binding.seeFileButton.setVisibility(View.VISIBLE);
}
}
});
Paso 4: Ejecuta el código
Ejecuta tu código. Deberías ver el nuevo botón See File en el que se puede hacer clic, que te llevará al archivo resultante:
Agregaste este botón Cancel Work. Ahora agreguemos el código para que haga algo. Con WorkManager, puedes cancelar trabajos usando el ID, por etiqueta y por nombre de cadena único.
En este caso, querrás cancelar el trabajo por nombre de cadena único, ya que quieres cancelar todo el trabajo de la cadena, no solo un paso en particular.
Paso 1: Cancela el trabajo por nombre
En el modelo de vista, escribe el método para cancelar el trabajo:
BlurViewModel.java
/**
* Cancel work using the work's unique name
*/
void cancelWork() {
mWorkManager.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.java
// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener(view -> mViewModel.cancelWork());
Paso 3: Ejecuta y cancela tu trabajo
Ejecuta la app. Debería compilarse bien. Empieza a desenfocar una imagen y, luego, haz clic en el botón Cancelar. Se cancelará toda la cadena.
Por último, pero no menos importante, WorkManager
admite Constraints
. En el caso de Blur-O-Matic, usarás la restricción que establece que el dispositivo deberá estar cargándose cuando se realice la operación de guardado.
Paso 1: Crea y agrega la restricción de carga
Para crear un objeto Constraints
, deberás usar un Constraints.Builder
. Luego, deberás configurar las restricciones que deseas y agregarlas a la WorkRequest
, como se muestra a continuación:
BlurViewModel.java
// In the applyBlur method
// Create charging constraint
Constraints constraints = new Constraints.Builder()
.setRequiresCharging(true)
.build();
// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
.setConstraints(constraints) // This adds the Constraints
.addTag(TAG_OUTPUT)
.build();
continuation = continuation.then(save);
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á permanecer en el estado de carga hasta que lo conectes.
¡Felicitaciones! Terminaste la app de Blur-O-Matic y, en el proceso, aprendiste lo siguiente:
- Cómo agregar WorkManager a tu Proyecto
- Cómo programar un
OneOffWorkRequest
- Parámetros de entrada y salida
- Cómo encadenar el trabajo con
WorkRequest
- Cómo asignar nombres a cadenas de
WorkRequest
únicas - Cómo etiquetar
WorkRequest
- Cómo mostrar
WorkInfo
en la IU - Cómo cancelar
WorkRequest
- Cómo agregar restricciones a una
WorkRequest
¡Buen "trabajo"! Para ver el estado final del código y todos los cambios, consulta lo siguiente:
También puedes clonar el codelab de WorkManager desde GitHub:
$ git clone -b java 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. Si quieres obtener más información, consulta la documentación de WorkManager.