Trabalho em segundo plano com o WorkManager

1. Antes de começar

Este codelab abrange o WorkManager, uma biblioteca compatível com versões anteriores, flexível e simples para trabalhos em segundo plano adiáveis. O WorkManager é o programador de tarefas recomendado para Android para trabalhos adiáveis, com garantia de execução.

Pré-requisitos

O que você vai aprender

O que você vai fazer

  • Modificar um app inicial para usar o WorkManager.
  • Implementar uma solicitação de trabalho para desfocar uma imagem.
  • Implementar um grupo em série de trabalhos encadeados.
  • Transmitir dados para dentro e para fora do trabalho que está sendo agendado.

O que é necessário

  • A versão estável mais recente do Android Studio.
  • Uma conexão com a Internet.

2. Visão geral do app

Atualmente, os smartphones são muito bons para tirar fotos. Tirar fotos desfocadas de algo misterioso é coisa do passado.

Neste codelab, você vai trabalhar com o Blur-O-Matic, um app que desfoca fotos e salva os resultados em um arquivo. Aquilo era o monstro do Lago Ness ou um submarino de brinquedo (link em inglês)? Com o Blur-O-Matic, ninguém vai saber!

A tela tem botões de opção em que você pode selecionar o nível de desfoque da imagem. Clicar no botão Start (começar) desfoca e salva a imagem.

No momento, o app não aplica desfoque nem salva a imagem final.

O foco deste codelab é adicionar o WorkManager ao app, criar workers para limpar arquivos temporários criados para desfocar uma imagem, desfocar a imagem e salvar uma cópia final dela, que pode ser visualizada ao clicar no botão See File (mostrar arquivo). Você também vai aprender a monitorar o status do trabalho em segundo plano e a atualizar a interface do app.

3. Conhecer o app inicial Blur-O-Matic

Acessar o código inicial

Para começar, faça o download do código inicial:

Outra opção é clonar o repositório do GitHub:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout starter

Procure o código do app Blur-o-matic neste repositório do GitHub (em inglês).

Executar o código inicial

Para se familiarizar com o código inicial, siga estas etapas:

  1. Abra o projeto com o código inicial no Android Studio.
  2. Execute o app em um dispositivo Android ou em um emulador.

2bdb6fdc2567e96.png

A tela tem botões de opção que permitem selecionar o nível de desfoque da imagem. Quando você clica no botão Start, o app desfoca e salva a imagem.

No momento, o app não aplica desfoque quando você clica no botão Start.

Tutorial do código inicial

Nesta tarefa, você vai conhecer a estrutura do projeto. Confira abaixo explicações sobre os arquivos e as pastas mais importantes desse projeto.

  • WorkerUtils: métodos práticos que vão ser usados mais tarde para mostrar Notifications e o código para salvar um bitmap no arquivo.
  • BlurViewModel: esse modelo de visualização armazena o estado do app e interage com o repositório.
  • WorkManagerBluromaticRepository: a classe em que você inicia o trabalho em segundo plano com o WorkManager.
  • Constants: uma classe estática com algumas constantes usadas durante o codelab.
  • BluromaticScreen: contém funções combináveis para a interface e interage com o BlurViewModel. As funções combináveis mostram a imagem e incluem botões de opção para selecionar o nível de desfoque desejado.

4. O que é o WorkManager?

O WorkManager faz parte do Android Jetpack e de um componente de arquitetura para trabalho em segundo plano que requer uma combinação de execução oportunista e garantida. Execução oportunista significa que o WorkManager faz o trabalho em segundo plano o quanto antes. Já a execução garantida se refere à lógica para iniciar o trabalho em diversas situações, mesmo se você sair do app.

O WorkManager é uma biblioteca incrivelmente flexível, que oferece diversos outros benefícios. Alguns desses benefícios incluem:

  • Suporte a tarefas únicas e periódicas assíncronas.
  • Suporte a restrições, por exemplo, condições de rede, espaço de armazenamento e status de carregamento.
  • Encadeamento de solicitações de trabalho complexas, por exemplo, a execução de trabalhos em paralelo.
  • Usar a saída de uma solicitação de trabalho como entrada para a próxima.
  • Suporte a níveis anteriores da API até o 14 (confira a observação).
  • Trabalhar com ou sem o Google Play Services.
  • Segue as práticas recomendadas de integridade do sistema.
  • Oferece suporte à exibição facilitada do estado das solicitações de trabalho na interface do app.

5. Quando usar o WorkManager?

O WorkManager é uma boa opção para tarefas que precisam ser concluídas. A execução dessas tarefas não depende que o app continue em execução depois que o trabalho é colocado na fila. As tarefas são executadas mesmo se o app for fechado ou se o usuário retornar à tela inicial.

Alguns exemplos de tarefas que fazem um bom uso do WorkManager:

  • Consultar periodicamente as últimas notícias.
  • Aplicar filtros a uma imagem e salvá-la.
  • Sincronização periódica de dados locais com a rede.

O WorkManager é uma opção para executar uma tarefa fora da linha de execução principal, mas não é abrangente o suficiente para executar todos os tipos de tarefa fora da linha de execução principal. Corrotinas são uma outra opção discutida nos codelabs anteriores.

Para saber mais sobre quando usar o WorkManager, confira o Guia para o processamento em segundo plano.

6. Adicionar o WorkManager ao app

O WorkManager requer a dependência do Gradle mostrada abaixo. Ela já está incluída no arquivo de build.

app/build.gradle.kts

dependencies {
    // WorkManager dependency
    implementation("androidx.work:work-runtime-ktx:2.8.1")
}

É necessário usar a versão mais recente da versão estável do work-runtime-ktx no app.

Se você mudar a versão, clique em Sync Now para sincronizar o projeto com os arquivos atualizados do Gradle.

7. Noções básicas do WorkManager

Há algumas classes do WorkManager que você precisa conhecer:

  • Worker / CoroutineWorker: Worker é uma classe que executa o trabalho de forma síncrona em uma linha de execução em segundo plano. Como temos interesse no trabalho assíncrono, podemos usar a classe CoroutineWorker, que tem interoperabilidade com as corrotinas do Kotlin. Neste app, você estende a classe CoroutineWorker e substitui o método doWork(), em que você coloca o código do trabalho real que quer realizar em segundo plano.
  • WorkRequest: representa uma solicitação para realizar algum trabalho. Uma WorkRequest é o local onde você define se o worker precisa ser executado uma vez ou periodicamente. Também é possível impor restrições na WorkRequest que exigem determinadas condições antes da execução do trabalho. Por exemplo, que o dispositivo esteja carregando antes de iniciar o trabalho solicitado. Você transmite o CoroutineWorker como parte da criação da WorkRequest.
  • WorkManager: essa classe agenda e executa a WorkRequest. Ela programa uma WorkRequest de modo a distribuir a carga nos recursos do sistema, respeitando as restrições especificadas.

No seu caso, você define uma nova classe BlurWorker, que contém o código para desfocar uma imagem. Quando você clica no botão Start, o WorkManager cria e depois enfileira um objeto WorkRequest.

8. Criar o BlurWorker

Nesta etapa, você vai usar uma imagem na pasta res/drawable chamada android_cupcake.png e executar algumas funções nela em segundo plano. Essas funções desfocam a imagem.

  1. Clique com o botão direito do mouse no pacote com.example.bluromatic.workers no painel do projeto Android e selecione New -> Kotlin Class/File.
  2. Dê o nome BlurWorker à nova classe Kotlin. Estenda-a do CoroutineWorker com os parâmetros obrigatórios do construtor.

workers/BlurWorker.kt

import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import android.content.Context

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
}

A classe do BlurWorker estende a classe do CoroutineWorker, em vez da classe mais geral Worker. A implementação da classe CoroutineWorker do doWork() é uma função de suspensão, que permite executar um código assíncrono, o que não pode ser realizado por um Worker. Como detalhado no guia Linhas de execução no WorkManager, "CoroutineWorker é a implementação recomendada para usuários do Kotlin".

Nesse ponto, o Android Studio mostra uma linha ondulada vermelha em class BlurWorker, que indica um erro.

9e96aa94f82c6990.png

Se você colocar o cursor sobre o texto class BlurWorker, o ambiente de desenvolvimento integrado vai mostrar um pop-up com mais informações sobre o erro.

cdc4bbefa7a9912b.png

A mensagem de erro indica que você não substituiu o método doWork() conforme necessário.

No método doWork(), crie o código para desfocar a imagem mostrada do cupcake.

Siga estas etapas para corrigir o erro e implementar o método doWork():

  1. Posicione o cursor dentro do código da classe, clicando no texto "BlurWorker".
  2. No menu do Android Studio, selecione Code > Override Methods...
  3. No pop-up Override Members, selecione doWork().
  4. Clique em OK.

8f495f0861ed19ff.png

  1. Imediatamente antes da declaração de classe, crie uma variável com o nome TAG e atribua a ela o valor BlurWorker. Essa variável não está relacionada especificamente ao método doWork(), mas é usada mais tarde em chamadas para Log().

workers/BlurWorker.kt

private const val TAG = "BlurWorker"

class BlurWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {
...
  1. Para ver melhor quando o trabalho é executado, você precisa usar a função makeStatusNotification() do WorkerUtil. Essa função permite que você mostre facilmente um banner de notificação na parte de cima da tela.

No método doWork(), use a função makeStatusNotification() para exibir uma notificação de status e informar ao usuário que o worker de desfoque foi iniciado e está desfocando a imagem.

workers/BlurWorker.kt

import com.example.bluromatic.R
...
override suspend fun doWork(): Result {

    makeStatusNotification(
        applicationContext.resources.getString(R.string.blurring_image),
        applicationContext
    )
...
  1. Adicione um bloco de código return try...catch, em que o trabalho real da imagem de desfoque é realizado.

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
        } catch (throwable: Throwable) {
        }
...
  1. No bloco try, adicione uma chamada para Result.success().
  2. No bloco catch, adicione uma chamada para Result.failure().

workers/BlurWorker.kt

...
        makeStatusNotification(
            applicationContext.resources.getString(R.string.blurring_image),
            applicationContext
        )

        return try {
            Result.success()
        } catch (throwable: Throwable) {
            Result.failure()
        }
...
  1. No bloco try, crie uma nova variável chamada picture e a preencha com o bitmap retornado da chamada do método BitmapFactory.decodeResource() e da transmissão do pacote de recursos do aplicativo e o ID do recurso da imagem do cupcake.

workers/BlurWorker.kt

...
        return try {
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            Result.success()
...
  1. Desfoque o bitmap chamando a função blurBitmap() e transmita a variável picture e um valor de 1 (um) para o parâmetro blurLevel.
  2. Salve o resultado em uma nova variável com o nome output.

workers/BlurWorker.kt

...
            val picture = BitmapFactory.decodeResource(
                applicationContext.resources,
                R.drawable.android_cupcake
            )

            val output = blurBitmap(picture, 1)

            Result.success()
...
  1. Crie uma nova variável outputUri e a preencha com uma chamada para a função writeBitmapToFile().
  2. Na chamada para writeBitmapToFile(), transmita o contexto do aplicativo e a variável output como argumentos.

workers/BlurWorker.kt

...
            val output = blurBitmap(picture, 1)

            // Write bitmap to a temp file
            val outputUri = writeBitmapToFile(applicationContext, output)

            Result.success()
...
  1. Adicione o código para mostrar uma mensagem de notificação ao usuário com a variável outputUri.

workers/BlurWorker.kt

...
            val outputUri = writeBitmapToFile(applicationContext, output)

            makeStatusNotification(
                "Output is $outputUri",
                applicationContext
            )

            Result.success()
...
  1. No bloco catch, registre uma mensagem indicando que ocorreu um erro ao tentar desfocar a imagem. A chamada para Log.e() transmite a variável TAG definida anteriormente, uma mensagem adequada e a exceção que está sendo gerada.

workers/BlurWorker.kt

...
        } catch (throwable: Throwable) {
            Log.e(
                TAG,
                applicationContext.resources.getString(R.string.error_applying_blur),
                throwable
            )
            Result.failure()
        }
...

Por padrão, um CoroutineWorker, é executado como Dispatchers.Default, mas pode ser mudado chamando withContext() e transmitindo o agente desejado.

  1. Crie um bloco withContext().
  2. Na chamada para withContext(), transmita Dispatchers.IO para que a função lambda seja executada em um pool de linhas de execução especial para possivelmente bloquear operações de E/S.
  3. Mova o código return try...catch criado anteriormente para esse bloco.
...
        return withContext(Dispatchers.IO) {

            return try {
                // ...
            } catch (throwable: Throwable) {
                // ...
            }
        }
...

O Android Studio mostra o erro abaixo porque não é possível chamar return em uma função lambda.

2d81a484b1edfd1d.png

Para corrigir esse erro, adicione um rótulo, conforme mostrado no pop-up.

...
            //return try {
            return@withContext try {
...

Como esse worker é executado muito rapidamente, é recomendável adicionar um atraso no código para emular um trabalho mais lento.

  1. No lambda withContext(), adicione uma chamada para a função utilitária delay() e transmita a constante DELAY_TIME_MILLIS. Essa chamada é apenas para que o codelab forneça um atraso entre as mensagens de notificação.
import com.example.bluromatic.DELAY_TIME_MILLIS
import kotlinx.coroutines.delay

...
        return withContext(Dispatchers.IO) {

            // This is an utility function added to emulate slower work.
            delay(DELAY_TIME_MILLIS)

                val picture = BitmapFactory.decodeResource(
...

9. Atualizar o WorkManagerBluromaticRepository

O repositório processa todas as interações com o WorkManager. Essa estrutura está de acordo com o princípio de design de separação de conceitos e é um padrão recomendado de arquitetura do Android.

  • No arquivo data/WorkManagerBluromaticRepository.kt, dentro da classe WorkManagerBluromaticRepository, crie uma variável particular com o nome workManager e armazene uma instância de WorkManager nela chamando WorkManager.getInstance(context).

data/WorkManagerBluromaticRepository.kt

import androidx.work.WorkManager
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    // New code
    private val workManager = WorkManager.getInstance(context)
...

Criar e enfileirar a WorkRequest no WorkManager

Muito bem. Agora vamos fazer uma WorkRequest e pedir para o WorkManager executá-la. Há dois tipos de WorkRequests:

  • OneTimeWorkRequest: uma WorkRequest que é executada apenas uma vez.
  • PeriodicWorkRequest: uma WorkRequest que é executada repetidas vezes em um ciclo.

É importante que a imagem seja desfocada uma vez, quando o usuário clicar no botão Start.

Esse trabalho ocorre no método applyBlur(), que é chamado quando você clica no botão Start.

As etapas abaixo são concluídas no método applyBlur().

  1. Para preencher uma nova variável com o nome blurBuilder, crie uma OneTimeWorkRequest para o worker de desfoque e chame a função de extensão OneTimeWorkRequestBuilder do WorkManager KTX.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
}
  1. Inicie o trabalho chamando o método enqueue() no objeto workManager.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.BlurWorker
import androidx.work.OneTimeWorkRequestBuilder
...
override fun applyBlur(blurLevel: Int) {
    // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // Start the work
    workManager.enqueue(blurBuilder.build())
}
  1. Execute o app e confira a notificação ao clicar no botão Start.

No momento, a imagem é desfocada no mesmo nível, independente da opção selecionada. Nas próximas etapas, o nível de desfoque vai mudar com base na opção selecionada.

f2b3591b86d1999d.png

Para confirmar se a imagem está sendo desfocada corretamente, abra o Device Explorer no Android Studio:

6bc555807e67f5ad.png

Depois navegue até data > data > com.example.bluromatic > files > blur_filter_outputs > <URI> e confirme se a imagem do cupcake está sendo desfocada:

fce43c920a61a2e3.png

10. Dados de entrada e saída

Desfocar o recurso de imagem no diretório de recursos funciona, mas, para que o Blur-O-Matic seja revolucionário como está destinado a ser, você precisa permitir que o usuário desfoque a imagem exibida na tela e depois mostre o resultado desfocado.

Para fazer isso, fornecemos o URI da imagem mostrada do cupcake como entrada para a WorkRequest e usamos a saída da WorkRequest para exibir a imagem desfocada final.

ce8ec44543479fe5.png

A entrada e a saída são transmitidas para um worker por objetos Data. Objetos Data são contêineres leves para pares de chave-valor. Eles armazenam uma pequena quantidade de dados que podem entrar e sair de um worker pela WorkRequest.

Na próxima etapa, você vai transmitir o URI para o BlurWorker criando um objeto de dados de entrada.

Criar um objeto de dados de entrada

  1. No arquivo data/WorkManagerBluromaticRepository.kt, dentro da classe WorkManagerBluromaticRepository, crie uma nova variável particular com o nome imageUri.
  2. Preencha a variável com o URI da imagem, chamando o método de contexto getImageUri().

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.getImageUri
...
class WorkManagerBluromaticRepository(context: Context) : BluromaticRepository {

    private var imageUri: Uri = context.getImageUri() // <- Add this
    private val workManager = WorkManager.getInstance(context)
...

O código do app contém a função auxiliar createInputDataForWorkRequest() para criar objetos de dados de entrada.

data/WorkManagerBluromaticRepository.kt

// For reference - already exists in the app
private fun createInputDataForWorkRequest(blurLevel: Int, imageUri: Uri): Data {
    val builder = Data.Builder()
    builder.putString(KEY_IMAGE_URI, imageUri.toString()).putInt(BLUR_LEVEL, blurLevel)
    return builder.build()
}

Primeiro, a função auxiliar cria um objeto Data.Builder. Em seguida, coloca imageUri e blurLevel nelas como pares de chave-valor. Um objeto de dados é criado e retornado quando chama return builder.build().

  1. Para definir o objeto de dados de entrada para a WorkRequest, chame o método blurBuilder.setInputData(). É possível criar e transmitir o objeto de dados em uma etapa chamando a função auxiliar createInputDataForWorkRequest() como argumento. Para chamar a função createInputDataForWorkRequest(), transmita as variáveis blurLevel e imageUri.

data/WorkManagerBluromaticRepository.kt

override fun applyBlur(blurLevel: Int) {
     // Create WorkRequest to blur the image
    val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()

    // New code for input data object
    blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))

    workManager.enqueue(blurBuilder.build())
}

Acessar o objeto de dados de entrada

Agora, vamos atualizar o método doWork() na classe BlurWorker para acessar o URI e o nível de desfoque transmitido pelo objeto de dados de entrada. Se um valor de blurLevel não tiver sido fornecido, o padrão vai ser 1.

No método doWork():

  1. Crie uma nova variável com o nome resourceUri e a preencha chamando inputData.getString() e transmitindo a constante KEY_IMAGE_URI usada como chave ao criar o objeto de dados de entrada.

val resourceUri = inputData.getString(KEY_IMAGE_URI)

  1. Crie uma nova variável com o nome blurLevel. Preencha a variável chamando inputData.getInt() e transmitindo a constante BLUR_LEVEL que foi usada como chave ao criar o objeto de dados de entrada. Caso esse par de chave-valor não tenha sido criado, forneça um valor padrão de 1 (um).

workers/BlurWorker.kt

import com.example.bluromatic.KEY_BLUR_LEVEL
import com.example.bluromatic.KEY_IMAGE_URI
...
override fun doWork(): Result {

    // ADD THESE LINES
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    val blurLevel = inputData.getInt(KEY_BLUR_LEVEL, 1)

    // ... rest of doWork()
}

Com o URI, agora vamos desfocar a imagem do cupcake na tela.

  1. Verifique se a variável resourceUri está preenchida. Se não estiver, uma exceção será gerada. O código abaixo usa a instrução require() (link em inglês), que gera uma IllegalArgumentException quando o primeiro argumento é falso.

workers/BlurWorker.kt

return@withContext try {
    // NEW code
    require(!resourceUri.isNullOrBlank()) {
        val errorMessage =
            applicationContext.resources.getString(R.string.invalid_input_uri)
            Log.e(TAG, errorMessage)
            errorMessage
    }

Como a origem da imagem é transmitida como um URI, precisamos de um objeto ContentResolver para ler o conteúdo apontado pelo URI.

  1. Adicione um objeto contentResolver ao valor applicationContext.

workers/BlurWorker.kt

...
    require(!resourceUri.isNullOrBlank()) {
        // ...
    }
    val resolver = applicationContext.contentResolver
...
  1. Como a origem da imagem agora é a transmitida em URI, use BitmapFactory.decodeStream() em vez de BitmapFactory.decodeResource() para criar o objeto Bitmap.

workers/BlurWorker.kt

import android.net.Uri
...
//     val picture = BitmapFactory.decodeResource(
//         applicationContext.resources,
//         R.drawable.android_cupcake
//     )

    val resolver = applicationContext.contentResolver

    val picture = BitmapFactory.decodeStream(
        resolver.openInputStream(Uri.parse(resourceUri))
    )
  1. Transmita a variável blurLevel na chamada da função blurBitmap().

workers/BlurWorker.kt

//val output = blurBitmap(picture, 1)
val output = blurBitmap(picture, blurLevel)

Criar o objeto de dados de saída

Você terminou esse worker e pode retornar o URI de saída como um objeto de dados de saída em Result.success(). Fornecer o URI de saída como um objeto de dados de saída facilita o acesso a outros workers para mais operações. Essa abordagem vai ser útil na próxima seção durante a criação de uma cadeia de workers.

Para isso, siga as etapas abaixo:

  1. Antes do código Result.success(), crie uma nova variável com o nome outputData.
  2. Para preencher essa variável, chame a função workDataOf() e use a constante KEY_IMAGE_URI para a chave e a variável outputUri como o valor. A função workDataOf() cria um objeto de dados do par de chave-valor transmitido.

workers/BlurWorker.kt

import androidx.work.workDataOf
// ...
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
  1. Atualize o código Result.success() para usar esse novo objeto de dados como argumento.

workers/BlurWorker.kt

//Result.success()
Result.success(outputData)
  1. Remova o código que mostra a notificação, já que ela não é mais necessária, porque o objeto de dados de saída agora usa o URI.

workers/BlurWorker.kt

// REMOVE the following notification code
//makeStatusNotification(
//    "Output is $outputUri",
//    applicationContext
//)

Executar seu app

Nesse ponto, ao executar o app, você espera que ele seja compilado. A imagem desfocada aparece no Device Explorer, mas ainda não é mostrada na tela.

Talvez seja necessário usar a função Synchronize para mostrar as imagens:

a658ad6e65f0ce5d.png

Bom trabalho! Você desfocou uma imagem de entrada usando o WorkManager.

11. Encadear seu trabalho

No momento, você está realizando uma única tarefa: desfocar a imagem. Essa tarefa é uma ótima etapa inicial, mas o app ainda não tem algumas funcionalidades importantes:

  • O app não limpa arquivos temporários.
  • O app não salva a imagem em um arquivo permanente.
  • O app sempre desfoca a imagem no mesmo nível.

Você pode usar uma cadeia de trabalho do WorkManager para adicionar essa funcionalidade. O WorkManager permite que você crie WorkerRequests separadas que são executadas em ordem ou em paralelo.

Nesta seção, você vai criar uma cadeia de trabalho parecida com esta:

c883bea5a5beac45.png

As caixas representam as WorkRequests.

Outro recurso de encadeamento é a capacidade de aceitar entrada e produzir saída. A saída de uma WorkRequest se torna a entrada da próxima WorkRequest na cadeia.

Você já tem um CoroutineWorker para desfocar uma imagem, mas também precisa de um CoroutineWorker para limpar arquivos temporários e um CoroutineWorker para salvar a imagem de forma permanente.

Criar o CleanupWorker

O CleanupWorker exclui os arquivos temporários, se houver.

  1. Clique com o botão direito do mouse no pacote com.example.bluromatic.workers no painel do projeto Android e selecione New -> Kotlin Class/File.
  2. Dê o nome CleanupWorker à nova classe Kotlin.
  3. Copie o código do CleanupWorker.kt, conforme mostrado no exemplo de código abaixo.

Como a manipulação de arquivos está fora do escopo deste codelab, você pode copiar o código abaixo para o CleanupWorker.

workers/CleanupWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.OUTPUT_PATH
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"

class CleanupWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    override suspend fun doWork(): Result {
        /** Makes a notification when the work starts and slows down the work so that it's easier
         * to see each WorkRequest start, even on emulated devices
         */
        makeStatusNotification(
            applicationContext.resources.getString(R.string.cleaning_up_files),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

            return@withContext try {
                val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
                if (outputDirectory.exists()) {
                    val entries = outputDirectory.listFiles()
                    if (entries != null) {
                        for (entry in entries) {
                            val name = entry.name
                            if (name.isNotEmpty() && name.endsWith(".png")) {
                                val deleted = entry.delete()
                                Log.i(TAG, "Deleted $name - $deleted")
                            }
                        }
                    }
                }
                Result.success()
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_cleaning_file),
                    exception
                )
                Result.failure()
            }
        }
    }
}

Criar o SaveImageToFileWorker

A classe SaveImageToFileWorker salva o arquivo temporário em um permanente.

O SaveImageToFileWorker recebe a entrada e saída. A entrada é uma String do URI de imagem temporariamente desfocado, armazenada com a chave KEY_IMAGE_URI. A saída é uma String do URI da imagem desfocada salvo, armazenado com a chave KEY_IMAGE_URI.

de0ee97cca135cf8.png

  1. Clique com o botão direito do mouse no pacote com.example.bluromatic.workers no painel do projeto Android e selecione New -> Kotlin Class/File.
  2. Dê o nome SaveImageToFileWorker à nova classe Kotlin.
  3. Copie o código do SaveImageToFileWorker.kt conforme mostrado no código de exemplo abaixo.

Como a manipulação de arquivos está fora do escopo deste codelab, você pode copiar o código abaixo para o SaveImageToFileWorker. No código fornecido, observe como os valores resourceUri e output são extraídos e armazenados com a chave KEY_IMAGE_URI. Esse processo é muito semelhante ao código criado anteriormente para os objetos de dados de entrada e saída.

workers/SaveImageToFileWorker.kt

package com.example.bluromatic.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import com.example.bluromatic.DELAY_TIME_MILLIS
import com.example.bluromatic.KEY_IMAGE_URI
import com.example.bluromatic.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.Date

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"

class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : CoroutineWorker(ctx, params) {

    private val title = "Blurred Image"
    private val dateFormatter = SimpleDateFormat(
        "yyyy.MM.dd 'at' HH:mm:ss z",
        Locale.getDefault()
    )

    override suspend fun doWork(): Result {
        // Makes a notification when the work starts and slows down the work so that
        // it's easier to see each WorkRequest start, even on emulated devices
        makeStatusNotification(
            applicationContext.resources.getString(R.string.saving_image),
            applicationContext
        )

        return withContext(Dispatchers.IO) {
            delay(DELAY_TIME_MILLIS)

            val resolver = applicationContext.contentResolver
            return@withContext try {
                val resourceUri = inputData.getString(KEY_IMAGE_URI)
                val bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri))
                )
                val imageUrl = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, title, dateFormatter.format(Date())
                )
                if (!imageUrl.isNullOrEmpty()) {
                    val output = workDataOf(KEY_IMAGE_URI to imageUrl)

                    Result.success(output)
                } else {
                    Log.e(
                        TAG,
                        applicationContext.resources.getString(R.string.writing_to_mediaStore_failed)
                    )
                    Result.failure()
                }
            } catch (exception: Exception) {
                Log.e(
                    TAG,
                    applicationContext.resources.getString(R.string.error_saving_image),
                    exception
                )
                Result.failure()
            }
        }
    }
}

Criar uma cadeia de trabalho

Atualmente, o código cria e executa apenas uma WorkRequest.

Nesta etapa, você vai modificar o código para criar e executar uma cadeia de WorkRequests, em vez de apenas uma solicitação de imagem de desfoque.

Na cadeia de WorkRequests, sua primeira solicitação de trabalho é limpar os arquivos temporários.

  1. Em vez de chamar OneTimeWorkRequestBuilder, chame workManager.beginWith().

Chamar o método beginWith() retorna um objeto WorkContinuation e cria o ponto de partida para uma cadeia de WorkRequests com a primeira solicitação de trabalho.

data/WorkManagerBluromaticRepository.kt

import androidx.work.OneTimeWorkRequest
import com.example.bluromatic.workers.CleanupWorker
// ...
    override fun applyBlur(blurLevel: Int) {
        // Add WorkRequest to Cleanup temporary images
        var continuation = workManager.beginWith(OneTimeWorkRequest.from(CleanupWorker::class.java))

        // Add WorkRequest to blur the image
        val blurBuilder = OneTimeWorkRequestBuilder<BlurWorker>()
...

É possível adicionar mais solicitações de trabalho a essa cadeia chamando o método then() e transmitindo um objeto WorkRequest.

  1. Remova a chamada workManager.enqueue(blurBuilder.build()), que colocava apenas uma solicitação de trabalho na fila.
  2. Adicione a próxima solicitação de trabalho à cadeia, chamando o método .then().

data/WorkManagerBluromaticRepository.kt

...
//workManager.enqueue(blurBuilder.build())

// Add the blur work request to the chain
continuation = continuation.then(blurBuilder.build())
...
  1. Crie uma solicitação de trabalho para salvar a imagem e a adicionar à cadeia.

data/WorkManagerBluromaticRepository.kt

import com.example.bluromatic.workers.SaveImageToFileWorker

...
continuation = continuation.then(blurBuilder.build())

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
    .build()
continuation = continuation.then(save)
...
  1. Para iniciar o trabalho, chame o método enqueue() no objeto de continuação.

data/WorkManagerBluromaticRepository.kt

...
continuation = continuation.then(save)

// Start the work
continuation.enqueue()
...

Esse código produz e executa esta cadeia de WorkRequests: uma WorkRequest do CleanupWorker seguida por uma WorkRequest do BlurWorker seguida por uma WorkRequest do SaveImageToFileWorker.

  1. Execute o app.

Agora você pode clicar em Start e conferir as notificações quando os diferentes workers forem executados. A imagem desfocada ainda está disponível no Device Explorer. Em uma próxima seção, você vai adicionar um botão extra para que os usuários possam conferir a imagem desfocada no dispositivo.

Nas capturas de tela abaixo, observe que a mensagem de notificação mostra qual worker está em execução.

bbe0fdd79e3bca27.png

5d43bbfff1bfebe5.png

da2d31fa3609a7b1.png

Observe que a pasta de saída contém várias imagens desfocadas, ou seja, imagens que estão em estágios desfocados intermediários, e a imagem final mostra a imagem com o nível de desfoque selecionado.

Ótimo trabalho! Agora você pode limpar os arquivos temporários, desfocar e salvar uma imagem.

12. Acessar o código da solução

Para fazer o download do código do codelab concluído, use estes comandos:

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git
$ cd basic-android-kotlin-compose-training-workmanager
$ git checkout intermediate

Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.

Confira o código da solução deste codelab no GitHub (link em inglês).

13. Conclusão

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

  • adicionar a WorkManager ao projeto;
  • programar uma OneTimeWorkRequest;
  • usar parâmetros de entrada e saída;
  • Encadear trabalhos com WorkRequests.

O WorkManager envolve muito mais do que o conteúdo abordado neste codelab, incluindo trabalho repetitivo, uma biblioteca de suporte para testes, solicitações de trabalho paralelas e mesclagem de entradas.

Para saber mais, acesse a documentação Agendar tarefas com o WorkManager.