WorkManager avanzado

En este codelab, se enseñan conceptos avanzados de WorkManager. Se creó a partir del material abarcado en el codelab Trabajo en segundo plano con WorkManager.

A continuación, se incluyen otros recursos disponibles para que te familiarices con WorkManager:

Qué compilarás

En este codelab, trabajarás en Blur-O-Matic, una app que desenfoca fotos e imágenes, y guarda el resultado en un archivo. Si ya completaste el codelab Trabajo en segundo plano con WorkManager, verás que esta es la misma app de ejemplo. Ahora, agregarás algunas funciones al código:

  1. Configuración personalizada
  2. API de progreso para actualizar la IU mientras se ejecuta tu trabajo
  3. Prueba de tus trabajadores

Requisitos

Para realizar este codelab, necesitarás la versión estable de Android Studio más reciente.

También debes estar familiarizado con LiveData, ViewModel y View Binding. 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 (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 descargar el código final de Blur-o-Matic

Si lo prefieres, también puedes clonar el codelab de WorkManager completado desde GitHub:

$ git clone -b advanced https://github.com/googlecodelabs/android-workmanager

Paso 1: Descarga el código

Haz clic en el siguiente vínculo para descargar la versión del código que deberás seguir durante este codelab:

Descargar código inicial

Si lo prefieres, también puedes clonar el codelab desde GitHub:

$ git clone https://github.com/googlecodelabs/android-workmanager

Paso 2: Ejecuta la app

Ejecuta la app. Deberías ver las siguientes pantallas: Asegúrate de darle permiso a la app para que acceda a tus fotos cuando se te solicite.

Puedes seleccionar una imagen e ir a la pantalla siguiente, que tiene botones de selección con los que podrás elegir cuánto desenfocar tu imagen. Si presionas el botón Go, la imagen se desenfocará y se guardará. Durante el proceso, la app muestra el botón Cancel para que puedas detener el trabajo.

d6b8946f437ec4e1.png

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.
  • BlurApplication***:** Es la clase de aplicación con un método onCreate() simple para inicializar el sistema de registro Timber para compilaciones de depuración.
  • BlurActivity***:** 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 iniciarás el trabajo en segundo plano con WorkManager.
  • Workers/CleanupWorker**:** Este trabajador siempre borra los archivos temporales que existan.
  • Workers/BlurWorker***:** Este trabajador desenfoca la imagen que se pasa como datos de entrada con un URI y muestra el URI del archivo temporal.
  • Workers/SaveImageToFileWorker**:** Este trabajador toma como entrada el URI de la imagen temporal y muestra el URI del archivo final.
  • 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 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:

app/build.gradle

dependencies {
    implementation "androidx.work:work-runtime-ktx:$versions.work"
}

Debes obtener la versión más reciente de work-runtime en la página de la versión de WorkManager e indicar el número de la versión estable más reciente o usar la que se indica a continuación:

build.gradle

versions.work = "2.4.0"

Asegúrate de hacer clic en Sync Now para sincronizar tu proyecto con los archivos Gradle modificados.

En este paso, agregarás una configuración personalizada a la app a fin de modificar el nivel de registro de WorkManager para las compilaciones de depuración.

Paso 1: Inhabilita la inicialización predeterminada

Como se describe en la documentación de Configuración e inicialización personalizadas de WorkManager, debes inhabilitar la inicialización predeterminada en tu archivo AndroidManifest.xml. Para ello, quita de la biblioteca de WorkManager el nodo que se combina automáticamente de forma predeterminada.

Para quitar este nodo, puedes agregar un nuevo nodo de proveedor a tu AndroidManifest.xml, como se muestra a continuación:

AndroidManifest.xml

<application

...

    <provider
        android:name="androidx.work.impl.WorkManagerInitializer"
        android:authorities="${applicationId}.workmanager-init"
        tools:node="remove" />
</application>

También deberás agregar el espacio de nombres de las herramientas al manifiesto. El archivo completo con estos cambios será el siguiente:

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 -->

<manifest package="com.example.background"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <application
        android:name=".BlurApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".SelectImageActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <activity android:name=".BlurActivity" />

        <!-- ADD THE FOLLOWING NODE -->
        <provider
            android:name="androidx.work.impl.WorkManagerInitializer"
            android:authorities="${applicationId}.workmanager-init"
            tools:node="remove" />
    </application>
</manifest>

Paso 2: Agrega un Configuration.Provider a la clase Application

Para usar una inicialización a pedido, implementa la interfaz de Configuration.Provider de WorkManager en tu clase Application. La primera vez que tu app obtiene la instancia de WorkManager mediante getInstance(context), WorkManager se inicializa utilizando la configuración que muestra getWorkManagerConfiguration().

BlurApplication.kt

class BlurApplication : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration =

        Configuration.Builder()
                     .setMinimumLoggingLevel(android.util.Log.DEBUG)
                     .build()
...
}

Con este cambio, WorkManager se ejecuta con el registro establecido en DEBUG.

Una opción más conveniente es configurar WorkManager de esta manera solo para las compilaciones de depuración de tu app, utilizando algo como lo siguiente:

BlurApplication.kt

class BlurApplication() : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        return if (BuildConfig.DEBUG) {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.DEBUG)
                    .build()
        } else {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.ERROR)
                    .build()
        }
    }

...
}

Una vez completo, BlurApplication.kt queda de la siguiente manera:

BlurApplication.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background

import android.app.Application
import androidx.work.Configuration
import timber.log.Timber
import timber.log.Timber.DebugTree

class BlurApplication() : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration {
        return if (BuildConfig.DEBUG) {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.DEBUG)
                    .build()
        } else {
            Configuration.Builder()
                    .setMinimumLoggingLevel(android.util.Log.ERROR)
                    .build()
        }
    }

    override fun onCreate() {
        super.onCreate()
        if (BuildConfig.DEBUG) {
            Timber.plant(DebugTree())
        }
    }
}

Paso 3: Ejecuta la app en modo de depuración

Ahora, WorkManager está configurado para que tus compilaciones de depuración registren todos los mensajes provenientes de la biblioteca.

Cuando se ejecuta la app, puedes ver los registros en la pestaña logcat de Android Studio:

5f3522812d1bfb18.png

Paso 4: ¿Qué puedes configurar?

La lista completa de parámetros se encuentra en la guía de referencia de WorkManager para Configuration.Builder. Presta atención a dos parámetros adicionales:

  • WorkerFactory
  • Rango de JobId

Modificar WorkerFactory te permite agregar otros parámetros al constructor de tu trabajador. Puedes encontrar más información sobre cómo implementar una instancia WorkerFactory personalizada en este artículo sobre cómo personalizar WorkManager. Si usas WorkManager y la API de JobScheduler en tu app, te recomendamos personalizar el rango de JobId para evitar que las dos API usen el mismo rango de JobId. También hay una regla de lint que abarca este caso que se introdujo en v2.4.0.

WorkManager v2.3 agregó la funcionalidad para compartir información del progreso de tu trabajador a tu app mediante setProgressAsync() (o setProgress() cuando se utiliza desde CoroutineWorker). Esta información se puede observar a través de WorkInfo, y está previsto que se la utilice para brindar comentarios al usuario en la IU. Luego, los datos de progreso se cancelan cuando el trabajador alcanza un estado final (SUCCEEDED, FAILED o CANCELLED). Para obtener más información sobre cómo publicar y observar el progreso, consulta Cómo observar el progreso intermedio de un trabajador.

Ahora, agregarás una barra de progreso en la IU para que, si la app se encuentra en primer plano, el usuario pueda ver cómo avanza el desenfoque. El resultado final será el siguiente:

3ca52d773a4d0e8f.png

Paso 1: Modifica ProgressBar

Para modificar la ProgressBar en el diseño, debes borrar el parámetro android:indeterminate="true", agregar el estilo style="@android:style/Widget.ProgressBar.Horizontal", y establecer un valor inicial con android:progress="0". También debes establecer la orientación de LinearLayout en "vertical":

app/src/main/res/layout/activity_blur.xml

<LinearLayout
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:orientation="vertical">
    <ProgressBar
        android:id="@+id/progress_bar"
        style="@android:style/Widget.ProgressBar.Horizontal"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:progress="0"
        android:visibility="gone"
        android:layout_gravity="center_horizontal"
        />

    <Button
        android:id="@+id/cancel_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/cancel_work"
        android:visibility="gone"
        />
</LinearLayout>

También debes asegurarte de que la ProgressBar se reinicie en la posición inicial. Para ello, actualiza la función showWorkFinished() en el archivo BlurActivity.kt:

app/src/main/java/com/example/background/BlurActivity.kt

/**
 * Shows and hides views for when the Activity is done processing an image
 */
private fun showWorkFinished() {
    with(binding) {
        progressBar.visibility = View.GONE
        cancelButton.visibility = View.GONE
        goButton.visibility = View.VISIBLE
        progressBar.progress = 0 // <-- ADD THIS LINE
    }
}

Paso 2: Observa la información de progreso en ViewModel

Ya existe un observador en el archivo BlurViewModel que comprueba si se completó tu cadena. Agrega uno nuevo que observe el progreso publicado por BlurWorker.

Primero, agrega algunas constantes para hacer un seguimiento de esto al final del archivo Constants.kt:

app/src/main/java/com/example/background/Constants.kt

// Progress Data Key
const val PROGRESS = "PROGRESS"
const val TAG_PROGRESS = "TAG_PROGRESS"

El siguiente paso es agregar esta etiqueta a la WorkRequest de BlurWorker en el archivo BlurViewModel.kt para que puedas recuperar su WorkInfo. Desde ese WorkInfo, puedes recuperar la información de progreso del trabajador:

app/src/main/java/com/example/background/BlurViewModel.kt

// 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())
    }

    blurBuilder.addTag(TAG_PROGRESS) // <-- ADD THIS
    continuation = continuation.then(blurBuilder.build())
}

Agrega un nuevo LiveData al archivo BlurViewModel.kt que haga un seguimiento de esta WorkRequest, e inicializa los LiveData en el bloque init:

app/src/main/java/com/example/background/BlurViewModel.kt

class BlurViewModel(application: Application) : AndroidViewModel(application) {

    internal var imageUri: Uri? = null
    internal var outputUri: Uri? = null
    internal val outputWorkInfoItems: LiveData<List<WorkInfo>>
    internal val progressWorkInfoItems: LiveData<List<WorkInfo>> // <-- ADD THIS
    private val workManager: WorkManager = WorkManager.getInstance(application)

    init {
        // This transformation makes sure that whenever the current work Id changes the WorkStatus
        // the UI is listening to changes
        outputWorkInfoItems = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
        progressWorkInfoItems = workManager.getWorkInfosByTagLiveData(TAG_PROGRESS) // <-- ADD THIS
    }

...
}

Paso 3: Observa LiveData en la actividad

Ahora puedes usar estos LiveData en la BlurActivity para observar todo el progreso publicado. Primero, registra un nuevo observador de LiveData al final del método onCreate():

app/src/main/java/com/example/background/BlurActivity.kt

// Show work status
viewModel.outputWorkInfoItems.observe(this, outputObserver())

// ADD THE FOLLOWING LINES
// Show work progress
viewModel.progressWorkInfoItems.observe(this, progressObserver())

Ahora puedes verificar la WorkInfo que se recibió en el observador para ver si hay información de progreso y actualizar la ProgressBar según corresponda:

app/src/main/java/com/example/background/BlurActivity.kt

private fun progressObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        listOfWorkInfo.forEach { workInfo ->
            if (WorkInfo.State.RUNNING == workInfo.state) {
                val progress = workInfo.progress.getInt(PROGRESS, 0)
                binding.progressBar.progress = progress
            }
        }

    }
}

Paso 4: Publica el progreso desde BullWorker

Todas las piezas necesarias para mostrar la información de progreso ya están en su lugar. Es hora de agregar la publicación real de la información de progreso a BlurWorker.

En este ejemplo, simplemente se simula un proceso largo en nuestra función doWork() a fin de publicar información sobre el progreso durante un período definido.

El cambio aquí consiste en intercambiar una única demora por 10 más pequeñas y establecer un progreso nuevo en cada iteración:

app/src/main/java/com/example/background/workers/BlurWorker.kt

override fun doWork(): Result {
    val appContext = applicationContext

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)
    // sleep()
    (0..100 step 10).forEach {
        setProgressAsync(workDataOf(PROGRESS to it))
        sleep()
    }

...
}

Debido a que la demora original fue de 3 segundos, quizás te convenga reducirla por un factor de diez a 0.3 segundos:

app/src/main/java/com/example/background/Constants.kt

// const val DELAY_TIME_MILLIS: Long = 3000
const val DELAY_TIME_MILLIS: Long = 300

Paso 5: Ejecuta

Cuando se ejecuta la aplicación en este momento, debería mostrarse la ProgressBar propagada con los mensajes provenientes de BlurWorker.

Las pruebas son un componente importante de cada aplicación y, al agregar una biblioteca como WorkManager, es importante proporcionar las herramientas para probar tu código con facilidad.

Con WorkManager, también ofrecemos algunos asistentes que te permiten probar fácilmente a tus trabajadores. Para obtener más información sobre cómo crear pruebas para tus trabajadores, consulta la documentación de WorkManager sobre las pruebas.

En esta sección del codelab, presentaremos algunas pruebas para nuestras clases de Worker en las que se muestran algunos de los casos de uso más comunes.

Primero, queremos brindarte una manera sencilla de configurar nuestras pruebas. Para ello, podemos crear una TestRule que configure WorkManager:

  • Agregar dependencias
  • Crear WorkManagerTestRule y TestUtils
  • Crear prueba para CleanupWorker
  • Crear prueba para BlurWorker

Suponiendo que ya hayas creado la carpeta AndroidTest en tu proyecto, necesitamos agregar algunas dependencias para usar en nuestras pruebas:

app/build.gradle

androidTestImplementation "androidx.arch.core:core-testing:2.1.0"
androidTestImplementation "androidx.test.ext:junit:1.1.1"
androidTestImplementation "androidx.test:rules:1.2.0"
androidTestImplementation "androidx.test:runner:1.2.0"
androidTestImplementation "androidx.work:work-testing:$versions.work"

Ahora podemos comenzar a reunir las piezas con una TestRule que podemos usar en nuestras pruebas:

app/src/androidTest/java/com/example/background/workers/WorkManagerTestRule.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import android.content.Context
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.WorkManager
import androidx.work.testing.SynchronousExecutor
import androidx.work.testing.WorkManagerTestInitHelper
import org.junit.rules.TestWatcher
import org.junit.runner.Description

class WorkManagerTestRule : TestWatcher() {
    lateinit var targetContext: Context
    lateinit var testContext: Context
    lateinit var configuration: Configuration
    lateinit var workManager: WorkManager

    override fun starting(description: Description?) {
        targetContext = InstrumentationRegistry.getInstrumentation().targetContext
        testContext = InstrumentationRegistry.getInstrumentation().context
        configuration = Configuration.Builder()
                // Set log level to Log.DEBUG to make it easier to debug
                .setMinimumLoggingLevel(Log.DEBUG)
                // Use a SynchronousExecutor here to make it easier to write tests
                .setExecutor(SynchronousExecutor())
                .build()

        // Initialize WorkManager for instrumentation tests.
        WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, configuration)
        workManager = WorkManager.getInstance(targetContext)
    }
}

Debido a que necesitaremos esta imagen de prueba en el dispositivo (en el que se ejecutarán las pruebas), podemos crear algunas funciones auxiliares para usarlas en nuestras pruebas:

app/src/androidTest/java/com/example/background/workers/TestUtils.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import com.example.background.OUTPUT_PATH
import java.io.BufferedInputStream
import java.io.BufferedOutputStream
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.util.UUID

/**
 * Copy a file from the asset folder in the testContext to the OUTPUT_PATH in the target context.
 * @param testCtx android test context
 * @param targetCtx target context
 * @param filename source asset file
 * @return Uri for temp file
 */
@Throws(Exception::class)
fun copyFileFromTestToTargetCtx(testCtx: Context, targetCtx: Context, filename: String): Uri {
    // Create test image
    val destinationFilename = String.format("blur-test-%s.png", UUID.randomUUID().toString())
    val outputDir = File(targetCtx.filesDir, OUTPUT_PATH)
    if (!outputDir.exists()) {
        outputDir.mkdirs()
    }
    val outputFile = File(outputDir, destinationFilename)

    val bis = BufferedInputStream(testCtx.assets.open(filename))
    val bos = BufferedOutputStream(FileOutputStream(outputFile))
    val buf = ByteArray(1024)
    bis.read(buf)
    do {
        bos.write(buf)
    } while (bis.read(buf) != -1)
    bis.close()
    bos.close()

    return Uri.fromFile(outputFile)
}

/**
 * Check if a file exists in the given context.
 * @param testCtx android test context
 * @param uri for the file
 * @return true if file exist, false if the file does not exist of the Uri is not valid
 */
fun uriFileExists(targetCtx: Context, uri: String?): Boolean {
    if (uri.isNullOrEmpty()) {
        return false
    }

    val resolver = targetCtx.contentResolver

    // Create a bitmap
    try {
        BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(uri)))
    } catch (e: FileNotFoundException) {
        return false
    }
    return true
}

Una vez finalizado este trabajo, podemos comenzar a escribir nuestras pruebas.

Primero, probamos nuestro CleanupWorker, para verificar que realmente borre nuestros archivos. Para ello, durante la prueba, copia la imagen de prueba en el dispositivo y, luego, verifica que esté allí una vez que se haya ejecutado el CleanupWorker:

app/src/androidTest/java/com/example/background/workers/CleanupWorkerTest.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Rule
import org.junit.Test

class CleanupWorkerTest {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()
    @get:Rule
    var wmRule = WorkManagerTestRule()

    @Test
    fun testCleanupWork() {
        val testUri = copyFileFromTestToTargetCtx(
                wmRule.testContext, wmRule.targetContext, "test_image.png")
        assertThat(uriFileExists(wmRule.targetContext, testUri.toString()), `is`(true))

        // Create request
        val request = OneTimeWorkRequestBuilder<CleanupWorker>().build()

        // Enqueue and wait for result. This also runs the Worker synchronously
        // because we are using a SynchronousExecutor.
        wmRule.workManager.enqueue(request).result.get()
        // Get WorkInfo
        val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()

        // Assert
        assertThat(uriFileExists(wmRule.targetContext, testUri.toString()), `is`(false))
        assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    }
}

Ahora puedes ejecutar esta prueba en Android Studio desde el menú de ejecución o mediante el rectángulo verde a la izquierda de tu clase de prueba:

be955a84b5b00400.png

También puedes ejecutar tus pruebas desde la línea de comandos utilizando el comando ./gradlew cAT de la carpeta raíz de tu proyecto.

Verás que tus pruebas se ejecutan correctamente.

Luego, podemos probar nuestro BlurWorker. Este trabajador espera datos de entrada con el URI de la imagen para procesar. Por lo tanto, podemos crear algunas pruebas: una que verifique que el trabajador falla si no hay un URI de entrada y otra que efectivamente procese la imagen de entrada.

app/src/androidTest/java/com/example/background/workers/BlurWorkerTest.kt

/* Copyright 2020 Google LLC.
   SPDX-License-Identifier: Apache-2.0 */

package com.example.background.workers

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkInfo
import androidx.work.workDataOf
import org.hamcrest.CoreMatchers.`is`
import org.junit.Assert.assertThat
import org.junit.Rule
import com.example.background.KEY_IMAGE_URI
import org.junit.Test

class BlurWorkerTest {

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()
    @get:Rule
    var wmRule = WorkManagerTestRule()

    @Test
    fun testFailsIfNoInput() {
        // Define input data

        // Create request
        val request = OneTimeWorkRequestBuilder<BlurWorker>().build()

        // Enqueue and wait for result. This also runs the Worker synchronously
        // because we are using a SynchronousExecutor.
        wmRule.workManager.enqueue(request).result.get()
        // Get WorkInfo
        val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()

        // Assert
        assertThat(workInfo.state, `is`(WorkInfo.State.FAILED))
    }

    @Test
    @Throws(Exception::class)
    fun testAppliesBlur() {
        // Define input data
        val inputDataUri = copyFileFromTestToTargetCtx(
                wmRule.testContext,
                wmRule.targetContext,
                "test_image.png")
        val inputData = workDataOf(KEY_IMAGE_URI to inputDataUri.toString())

        // Create request
        val request = OneTimeWorkRequestBuilder<BlurWorker>()
                .setInputData(inputData)
                .build()

        // Enqueue and wait for result. This also runs the Worker synchronously
        // because we are using a SynchronousExecutor.
        wmRule.workManager.enqueue(request).result.get()
        // Get WorkInfo
        val workInfo = wmRule.workManager.getWorkInfoById(request.id).get()
        val outputUri = workInfo.outputData.getString(KEY_IMAGE_URI)

        // Assert
        assertThat(uriFileExists(wmRule.targetContext, outputUri), `is`(true))
        assertThat(workInfo.state, `is`(WorkInfo.State.SUCCEEDED))
    }
}

Si ejecutas estas pruebas, ambas deberían completarse correctamente.

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

  • Cómo crear una configuración personalizada
  • Cómo publicar el progreso de tu trabajador
  • Cómo mostrar el progreso del trabajo en la IU
  • Cómo escribir pruebas para tus trabajadores

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

Código final de Blur-o-Matic

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

$ git clone -b advanced https://github.com/googlecodelabs/android-workmanager

WorkManager admite mucho más de lo que podríamos abarcar en este codelab. Para obtener más información, consulta la documentación de WorkManager.