WorkManager avançado

1. Introdução

Este codelab ensina conceitos avançados do WorkManager. Ele se baseia no material básico abrangido no codelab Trabalho em segundo plano com o WorkManager.

Veja outros recursos disponíveis para se familiarizar com o WorkManager:

O que você vai criar

Neste codelab, você vai trabalhar no Blur-O-Matic, um app que desfoca fotos e imagens e salva o resultado em um arquivo. Se você já concluiu o codelab Trabalho em segundo plano com o WorkManager, esse é um app de exemplo semelhante. A única diferença é que ele permite selecionar sua própria imagem da galeria de fotos para desfocar. Aqui, você vai adicionar alguns recursos ao código:

  1. Configuração personalizada
  2. Usar a API Progress para atualizar a IU enquanto o trabalho é executado
  3. Testar os workers

Pré-requisitos

Para fazer este codelab, você precisará da versão estável mais recente do Android Studio.

Você também já deve conhecer LiveData, ViewModel e View Binding. Caso você ainda não conheça essas classes, confira o codelab Componentes compatíveis com ciclo de vida do Android (especificamente para ViewModel e LiveData) ou o codelab Room com View (uma apresentação dos componentes de arquitetura).

Se você não entender algum ponto

Se você não entender algum ponto deste codelab ou quiser ver o estado final do código,

Se preferir, clone o codelab do WorkManager concluído no GitHub:

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

2. Etapas da configuração

Etapa 1: fazer o download do código

Clique no link a seguir para fazer o download da versão do código que será usada neste codelab:

Se preferir, clone o codelab no GitHub:

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

Etapa 2: executar o app

Execute o app. Você verá as seguintes telas. Conceda permissão ao app para acessar suas fotos quando solicitado.

Tela inicial do app que solicita que o usuário selecione uma imagem da galeria de fotos.

Tela mostrada ao usuário após a imagem ser selecionada na galeria, com botões de opção para a quantidade de desfoque desejada e o botão "Go" para iniciar o processo de desfoque.

É possível selecionar uma imagem e ir para a próxima tela, que tem botões de opção onde você pode selecionar o nível de desfoque da imagem. Pressionar o botão Ir desfoca e salva a imagem. Durante o desfoque, o app mostra um botão Cancel para que você finalize o trabalho.

Solicitação do WorkManager em andamento com a notificação sendo mostrada na parte de cima da tela e o ícone de carregamento na de baixo.

O código inicial contém o seguinte:

  • WorkerUtils: essa classe contém o código para desfoque e alguns métodos práticos que você vai usar posteriormente para mostrar Notifications e deixar o app mais lento.
  • BlurApplication: classe do app com um método onCreate() simples para inicializar o sistema de geração de registros Timber para builds de depuração.
  • BlurActivity: a atividade que mostra a imagem e inclui botões de opção para selecionar o nível de desfoque.
  • BlurViewModel: esse modelo de visualização armazena todos os dados necessários para mostrar a BlurActivity. Também será a classe em que você vai iniciar o trabalho em segundo plano usando o WorkManager.
  • Workers/CleanupWorker: esse worker sempre exclui os arquivos temporários, se houver.
  • Workers/BlurWorker: esse worker desfoca a imagem transmitida como dados de entrada com um URI e retorna o URI do arquivo temporário.
  • Workers/SaveImageToFileWorker: esse worker usa como entrada o URI da imagem temporária e retorna o URI do arquivo final.
  • Constants: uma classe estática com algumas constantes que serão usadas durante o codelab.
  • SelectImageActivity: a primeira atividade que permite selecionar uma imagem.
  • res/activity_blur.xml e res/activity_select.xml: arquivos de layout de cada atividade.

Você vai fazer mudanças no código nas seguintes classes: BlurApplication, BlurActivity, BlurViewModel e BlurWorker.

3. Adicionar o WorkManager ao app

O WorkManager requer a dependência do Gradle abaixo. Os itens a seguir já foram incluídos nos arquivos:

app/build.gradle

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

Você precisa ter a versão mais atual do work-runtime, disponível na página de versões do WorkManager, e aplicar uma delas à versão estável mais recente ou usar uma das opções abaixo:

build.gradle

versions.work = "2.7.1"

Clique em Sync Now para sincronizar o projeto com os arquivos do Gradle modificados.

4. Adicionar uma configuração personalizada do WorkManager

Nesta etapa, você vai adicionar uma configuração personalizada ao app para modificar o nível de geração de registros do WorkManager para builds de depuração.

Etapa 1: desativar a inicialização padrão

Conforme descrito na documentação de Configuração e inicialização personalizadas do WorkManager, é preciso desativar a inicialização padrão no arquivo AndroidManifest.xml, removendo o nó mesclado automaticamente da biblioteca do WorkManager por padrão.

Para remover esse nó, você pode adicionar um novo nó de provedor ao AndroidManifest.xml, conforme mostrado abaixo:

AndroidManifest.xml

<application

...

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

Você também precisará adicionar o namespace das ferramentas ao manifesto. O arquivo completo com essas mudanças será:

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>

Etapa 2: adicionar um Configuration.Provider à classe Application

Você pode usar uma inicialização sob demanda implementando a interface Configuration.Provider do WorkManager na classe Application. Na primeira vez que o app receber a instância do WorkManager usando getInstance(context), o WorkManager será inicializado usando a configuração retornada por getWorkManagerConfiguration().

BlurApplication.kt

class BlurApplication : Application(), Configuration.Provider {

    override fun getWorkManagerConfiguration(): Configuration =

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

Com essa mudança, o WorkManager é executado com a geração de registros definida como DEBUG.

Uma opção melhor provavelmente será configurar o WorkManager dessa maneira apenas para builds de depuração do app, usando algo como:

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

...
}

Em seguida, BlurApplication.kt completo se torna:

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

Etapa 3: executar o app no modo de depuração

O WorkManager agora é configurado para que os builds de depuração registrem todas as mensagens da biblioteca.

Ao executar o app, é possível ver os registros na guia logcat do Android Studio:

5f3522812d1bfb18.png

Etapa 4: o que você pode configurar?

A lista completa de parâmetros está no guia de referência do WorkManager para Configuration.Builder. Preste atenção a dois parâmetros adicionais:

  • WorkerFactory
  • Intervalo JobId

Modificar o WorkerFactory permite adicionar outros parâmetros ao construtor do worker. Mais informações sobre como implementar um WorkerFactory personalizado estão disponíveis neste artigo sobre Como personalizar o WorkManager. Se você está usando o WorkManager e a API JobScheduler no app, é recomendável personalizar o intervalo JobId para evitar que o mesmo seja usado pelas duas APIs.

Compartilhamento do progresso do WorkManager

O WorkManager v2.3 adicionou a funcionalidade para compartilhar informações de progresso do worker com o app usando o setProgressAsync(), ou setProgress() quando usado em um CoroutineWorker. Essas informações podem ser observadas por uma classe WorkInfo e serão usadas para fornecer feedback ao usuário na IU. Os dados de progresso são cancelados quando o worker alcança um estado final (SUCCEEDED, FAILED ou CANCELLED). Para saber mais sobre como publicar e ouvir o progresso, leia Como observar o progresso intermediário do worker.

O que você fará agora é adicionar uma barra de progresso na IU para que, se o app estiver em primeiro plano, o usuário possa ver como está o desfoque. O resultado final será assim:

Solicitação do WorkManager em andamento, conforme indicado pela barra de progresso na parte de baixo da tela.

Etapa 1: modificar a ProgressBar

Para modificar a ProgressBar no layout, você precisa excluir o parâmetro android:indeterminate="true", adicionar o estilo style="@android:style/Widget.ProgressBar.Horizontal", e definir um valor inicial com android:progress="0". Também é necessário definir a orientação de LinearLayout como "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>

A outra mudança necessária é garantir que a ProgressBar seja reiniciada na posição inicial. Faça isso atualizando a função showWorkFinished() no arquivo 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
    }
}

Etapa 2: observar as informações de progresso no ViewModel

Já existe um observador no arquivo BlurViewModel que verifica quando a cadeia está completa. Adicione um novo para observar o progresso postado por BlurWorker.

Primeiro, adicione algumas constantes para rastrear isso no final do arquivo Constants.kt:

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

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

A próxima etapa é adicionar essa tag ao WorkRequest do BlurWorker no arquivo BlurViewModel.kt para que você possa recuperar WorkInfo. A partir de WorkInfo, é possível recuperar as informações de progresso do worker:

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

Adicione um novo LiveData ao arquivo BlurViewModel.kt que rastreie esse WorkRequest e inicialize o LiveData no bloco 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
    }

...
}

Etapa 3: observar o LiveData na atividade

Agora você pode usar esse LiveData em BlurActivity para observar todo o progresso publicado. Primeiro, registre um novo observador LiveData no final do 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())

Agora, você pode verificar a WorkInfo recebida no observador para ver se há informações de progresso e atualizar a ProgressBar conforme necessário:

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
            }
        }

    }
}

Etapa 4: publicar o progresso de BlurWorker

Todas as partes necessárias para exibir as informações de progresso já estão configuradas. É hora de adicionar a publicação real das informações de progresso ao BlurWorker.

Este exemplo simplesmente simula um processo longo na função doWork() para publicar informações de progresso durante um período definido.

A mudança feita aqui é trocar um único atraso por 10 menores, definindo um novo progresso em cada iteração:

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

...
}

Como o atraso original era de 3 segundos, provavelmente é uma boa ideia reduzi-lo para 10 de 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

Etapa 5: executar

A execução do app nesse ponto deve mostrar a ProgressBar preenchida com as mensagens provenientes de BlurWorker.

5. Testes do WorkManager

Testes são componentes importantes de todos os apps e, ao introduzir uma biblioteca como o WorkManager, é importante fornecer ferramentas para testar o código com facilidade.

Com o WorkManager, também disponibilizamos alguns auxiliares para testar facilmente os workers. Para saber mais sobre como criar testes para os workers, consulte a documentação do WorkManager sobre testes.

Nesta seção do codelab, apresentaremos alguns testes para as classes do worker com alguns dos casos de uso comuns.

Primeiro, vamos fornecer uma maneira fácil de configurar os testes. Para isso, podemos criar uma TestRule que configura o WorkManager:

  • Adicione dependências
  • Crie WorkManagerTestRule e TestUtils
  • Crie um teste para CleanupWorker
  • Crie um teste para BlurWorker

Se você já criou a pasta AndroidTest no seu projeto, precisamos adicionar algumas dependências para usar nos testes:

app/build.gradle

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

Agora, podemos começar juntar essas partes com uma TestRule que podemos usar nos testes:

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

Como precisaremos dessa imagem de teste no dispositivo (onde os testes serão executados), podemos criar algumas funções auxiliares para usar nos testes:

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
}

Quando terminarmos esse trabalho, começaremos a desenvolver os testes.

Primeiro, testamos o CleanupWorker para verificar se ele realmente exclui os arquivos. Para fazer isso, copie a imagem de teste no dispositivo do teste e verifique se ela está lá após a execução do 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))
    }
}

Agora, você pode executar este teste no Android Studio no menu "Executar" ou usando o retângulo verde no lado esquerdo da classe de teste:

be955a84b5b00400.png

Também é possível executar testes na linha de comando usando o comando ./gradlew cAT da pasta raiz do projeto.

Os testes devem ser executados corretamente.

Em seguida, podemos testar o BlurWorker. Esse worker espera dados de entrada com o URI da imagem para o processamento. Desse modo, podemos criar alguns testes: um que verifica se o worker falha quando não há URI de entrada e um que realmente processa a imagem 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))
    }
}

Se você executar esses testes, eles vão funcionar.

6. Parabéns

Parabéns! Você concluiu o app Blu-O-Matic e, no processo, aprendeu a:

  • Criar uma configuração personalizada
  • Publicar o progresso do worker
  • Exibir o progresso do trabalho na IU
  • Programar testes para workers

Bom "trabalho"! Para ver o estado final do código e todas as modificações, confira:

Se preferir, você pode clonar o codelab do WorkManager no GitHub:

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

O WorkManager envolve muito mais do que o conteúdo abordado neste codelab. Para saber mais, consulte a documentação do WorkManager.