Trabajos en segundo plano con WorkManager (Java)

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

Si en algún momento no puedes avanzar…

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

Descargar código final

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

$ git clone -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:

Descargar código inicial

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 mostrar Notifications y ralentizar la app.
  • BlurActivity***:** Esta es la actividad que muestra la imagen e incluye botones de selección para elegir el grado de desenfoque.
  • BlurViewModel***:** Este modelo de vista almacena todos los datos necesarios a fin de mostrar la BlurActivity. También será la clase en la que inicies el trabajo en segundo plano con WorkManager.
  • Constants**:** Esta es una clase estática con algunas constantes que usarás durante el codelab.
  • SelectImageActivity**:** Esta es la primera actividad que te permitirá seleccionar una imagen.
  • res/activity_blur.xml y res/activity_select.xml: Estos son los archivos de diseño de cada actividad.

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

WorkManager requiere la dependencia de Gradle que se indica a continuación. Estas ya se incluyeron en los archivos de compilación:

app/build.gradle

dependencies {
    // Other dependencies
    implementation "androidx.work:work-runtime:$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étodo doWork().
  • WorkRequest: Esta clase representa una solicitud para realizar algunos trabajos. Como parte de la creación de tu WorkRequest, pasarás el Worker. Cuando hagas la WorkRequest, también podrás especificar elementos como Constraints sobre el momento en que se debe ejecutar el Worker.
  • WorkManager: Esta clase programa tu WorkRequest y la ejecuta. Programa WorkRequest 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:

  1. Obtén un Context llamando a getApplicationContext(). Lo necesitarás para las diferentes manipulaciones de mapas de bits que estás por hacer.
  2. Crea un Bitmap a partir de la imagen de prueba:
Bitmap picture = BitmapFactory.decodeResource(
    applicationContext.getResources(),
    R.drawable.test);
  1. Obtén una versión desenfocada del mapa de bits llamando al método estático blurBitmap desde WorkerUtils.
  2. Escribe ese mapa de bits en un archivo temporal llamando al método estático writeBitmapToFile desde WorkerUtils. Asegúrate de guardar el URI que se muestra en una variable local.
  3. Realiza una notificación en la que se muestre el URI llamando al método estático makeStatusNotification desde WorkerUtils.
  4. Muestra Result.success();.
  5. Une el código de los pasos 2 a 6 en una sentencia try/catch. Captura un elemento Throwable genérico.
  6. En la sentencia de captura, emite una instrucción de registro de error: Log.e(TAG, "Error applying blur", throwable);.
  7. 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 una WorkRequest que solo se ejecutará una vez.
  • PeriodicWorkRequest: Es una WorkRequest que se repetirá en forma cíclica.

Solo queremos que la imagen se desenfoque una vez cuando se haga clic en el botón Go. Se llamará al método applyBlur cuando se haga clic en el botón Go, por lo que deberás crear una OneTimeWorkRequest desde BlurWorker. Luego, usa tu instancia de WorkManager para poner en cola tu WorkRequest.

Agrega la siguiente línea de código al método applyBlur() de BlurViewModel:

BlurViewModel.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.

7ef0320960f4d756.png

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

cf10a1af6e84f5ff.png

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

7f5eba3559b44cbb.png

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:

  1. Crearás un objeto Data.Builder.
  2. Si mImageUri es un URI no nulo, lo agregarás al objeto Data mediante el método putString. Este método toma una clave y un valor. Puedes usar la constante KEY_IMAGE_URI de string de la clase Constants.
  3. Llama a build() en el objeto Data.Builder a fin de crear tu objeto Data y mostrarlo.

A continuación, se muestra el método createInputDataForUri completo:

BlurViewModel.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:

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

A continuación, se muestra el método applyBlur completo:

BlurViewModel.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:

  1. Crea un nuevo objeto Data, tal como lo hiciste con la entrada, y almacena outputUri como una String. Usa la misma clave, KEY_IMAGE_URI.
  2. Pasa esto al método Result.success() de Worker.

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:

7e717ffd6b3d9d52.png

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

54832b34e9c9884a.png

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.

475a08a82ea675ca.png

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:

2c4bf31e5f6522ad.png

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:

e2d29f34bdf01860.png

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!

fcb326118dd99959.png

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:

En la siguiente tabla, se muestran tres formas diferentes de obtener objetos LiveData<WorkInfo> o LiveData<List<WorkInfo>>, así como lo que hace cada uno.

Tipo

Método de WorkManager

Descripción

Obtener trabajo con un ID

getWorkInfoByIdLiveData

Cada WorkRequest tiene un ID único generado por WorkManager. Puedes usarlo a fin de obtener un único LiveData
para esa WorkRequest exacta.

Obtener trabajo con un nombre de cadena único

getWorkInfosForUniqueWorkLiveData

Como acabas de ver, las WorkRequest pueden ser parte de una cadena única. Esto muestra el LiveData
>
de todo el trabajo en una sola cadena única de WorkRequests.

Obtener trabajo con una etiqueta

getWorkInfosByTagLiveData

Por último, también puedes etiquetar cualquier WorkRequest por medio de una String. Puedes etiquetar varias WorkRequest con la misma etiqueta a fin de asociarlas. Esto muestra el LiveData
>
de cualquier etiqueta individual.

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:

  1. Declara una nueva variable llamada mSavedWorkInfo, que es un LiveData<List<WorkInfo>>.
  2. En el constructor de BlurViewModel, obtén la WorkInfo mediante WorkManager.getWorkInfosByTagLiveData.
  3. 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:

  1. Verifica si la lista de WorkInfo no es nula y si tiene objetos WorkInfo en ella. De no ser así, entonces aún no se hizo clic en el botón Go. En ese caso, vuelve atrás.
  2. Obtén las primeras WorkInfo de la lista; solo habrá una WorkInfo etiquetada con TAG_OUTPUT, ya que hicimos que la cadena de trabajo fuera única.
  3. Comprueba si finalizó el estado del trabajo mediante workInfo.getState().isFinished();.
  4. Si no está terminado, llama a showWorkInProgress(), que ocultará y mostrará las vistas adecuadas.
  5. Cuando termine, llama al objeto showWorkFinished(), que ocultará y mostrará las vistas correspondientes.

Este es el código:

BlurActivity.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:

b7d8d3182f91ce23.png

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:

  1. Si WorkInfo finalizó, obtén los datos resultantes por medio de workInfo.getOutputData()..
  2. Luego, obtén el URI resultante. Recuerda que se almacena con la clave Constants.KEY_IMAGE_URI.
  3. Luego, si el URI no está vacío, se guardará correctamente. Muestra el seeFileButton y llama a setOutputUri 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:

992d0b2390600774.png

bc1dc9414fe2326e.png

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.

bdaadc9bb25472cb.png

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:

c2e56295cbe73f8.png

Cuando el dispositivo esté desconectado, deberá permanecer en el estado de carga hasta que lo conectes.

b7d8d3182f91ce23.png

¡Felicitaciones! Terminaste la app de Blur-O-Matic y, en el proceso, aprendiste lo siguiente:

  • Cómo agregar WorkManager a tu Proyecto
  • Cómo programar un OneOffWorkRequest
  • Parámetros de entrada y salida
  • Cómo encadenar el trabajo con WorkRequest
  • Cómo asignar nombres a cadenas de WorkRequest únicas
  • Cómo etiquetar WorkRequest
  • Cómo mostrar WorkInfo en la IU
  • Cómo cancelar WorkRequest
  • Cómo agregar restricciones a una WorkRequest

¡Buen "trabajo"! Para ver el estado final del código y todos los cambios, consulta lo siguiente:

Descargar código final

También puedes clonar el codelab de WorkManager desde GitHub:

$ git clone -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.