Trabalho em segundo plano com WorkManager (Kotlin)

1. Introdução

Existem muitas opções no Android para trabalhos em segundo plano adiáveis. Este codelab abrange a WorkManager, uma biblioteca compatível com versões anteriores, flexível e simples para trabalhos em segundo plano adiáveis. A WorkManager é a programadora de tarefas recomendada para usar no Android para trabalhos adiáveis, com garantia de execução.

O que é a WorkManager?

A 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. Na execução oportunista, a WorkManager fará o trabalho em segundo plano o quanto antes. Na execução garantida, ela cuidará da lógica para iniciar o trabalho em diversas situações, mesmo se você sair do app.

A WorkManager é uma biblioteca incrivelmente flexível, que oferece diversos outros benefícios. São eles:

  • Compatibilidade com tarefas únicas e periódicas assíncronas
  • Compatibilidade com restrições, como condições de rede, espaço de armazenamento e status de carregamento
  • Encadeamento de solicitações de trabalho complexas, incluindo a execução de trabalhos em paralelo
  • Saída de uma solicitação de trabalho usada como entrada para a próxima
  • Como gerenciar a compatibilidade de nível da API de volta até o nível 14. Consulte a observação.
  • Como trabalhar com ou sem o Google Play Services
  • Como seguir as práticas recomendadas de integridade do sistema
  • Compatibilidade do LiveData para exibir o estado da solicitação de trabalho de forma simples na IU

Quando usar a WorkManager?

A biblioteca WorkManager é uma boa opção para tarefas que oferecem uma conclusão útil, mesmo que o usuário saia da tela específica do seu app.

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

  • Upload de registros
  • Aplicação de filtros a imagens e salvamento da imagem
  • Sincronização periódica de dados locais com a rede

A WorkManager oferece execução garantida, mas nem todas as tarefas exigem isso. Por isso, ela não é uma exigência para executar todas as tarefas da linha de execução principal. Para saber mais sobre quando usar a WorkManager, confira o Guia para o processamento em segundo plano.

O que você criará

Atualmente, os smartphones são muito bons para tirar fotos. Os dias em que um fotógrafo conseguia tirar uma foto desfocada de algo misterioso ficaram no passado.

Neste codelab, você trabalhará no Blur-O-Matic, um app que desfoca fotos e imagens e salva o resultado em um arquivo. Aquilo era o monstro do Lago Ness ou um submarino de brinquedo? (em inglês). Com o Blur-O-Matic, ninguém jamais saberá.

Imagem do app concluído, com um marcador de imagem para cupcake, três opções de desfoque para aplicar à imagem e dois botões. Um para começar a desfocar a imagem e outro para vê-la.

Imagem desfocada conforme visto após clicar em "Ver arquivo".

O que você aprenderá

  • Como adicionar a WorkManager ao seu projeto
  • Como programar uma tarefa simples
  • Parâmetros de entrada e saída
  • Como fazer o encadeamento de trabalhos
  • Trabalhos únicos
  • Como exibir o status de trabalho na IU
  • Como cancelar trabalhos
  • Restrições de trabalho

Pré-requisitos

2. Etapas da configuração

Etapa 1: fazer o download do código

Clique no link abaixo para fazer o download de todo o código para este codelab:

Ou, se preferir, clone o codelab da WorkManager no GitHub:

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

Etapa 2: executar o app

Execute o app. Você verá a seguinte tela:

9e4707e0fbdd93c7.png

A tela precisa ter botões de opção para selecionar o nível de desfoque que você quer aplicar na imagem. Quando o botão Go for pressionado, a imagem será desfocada e salva.

Por enquanto, o app não aplica nenhum desfoque.

O código inicial contém:

  • WorkerUtils: essa classe contém o código para desfocar uma imagem e alguns métodos práticos que você usará posteriormente para exibir Notifications, salvar uma bitmap para arquivar e deixar o app mais lento.
  • 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 exibir BlurActivity. Também será a classe em que você iniciará o trabalho em segundo plano usando a WorkManager.
  • Constants: uma classe estática com algumas constantes que serão usadas durante o codelab.
  • res/activity_blur.xml: os arquivos de layout de BlurActivity.

***** Esses são os únicos arquivos em que você programará códigos.

3. Adicionar a WorkManager ao app

WorkManager requer a dependência do Gradle abaixo. Ela já foi incluída nos arquivos de compilação:

app/build.gradle

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

Clique aqui para encontrar a versão mais recente e estável de work-runtime-ktx e incluir a versão correta. No momento, a versão mais recente é:

build.gradle

versions.work = "2.7.1"

Se você atualizar sua versão para uma mais recente, use Sync Now para sincronizar seu projeto com os arquivos do Gradle alterados.

4. Criar sua primeira WorkRequest

Nesta etapa, você 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 e a salvam em um arquivo temporário.

Noções básicas da WorkManager

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

  • Worker: é nessa classe que você coloca o código do trabalho que quer realizar em segundo plano. Você ampliará essa classe e substituirá o método doWork().
  • WorkRequest: representa uma solicitação para realizar algum trabalho. Você transmitirá o Worker como parte da criação da WorkRequest. Ao criar a WorkRequest, você também pode especificar itens como Constraints ou quando o Worker deve ser executado.
  • WorkManager: essa classe programa suas WorkRequest e as executa. Ela programa WorkRequests de modo a distribuir a carga nos recursos do sistema, respeitando as restrições especificadas.

No seu caso, você definirá um novo BlurWorker, que conterá o código para desfocar uma imagem. Quando o botão Go for clicado, uma WorkRequest será criada e colocada na fila pela WorkManager.

Etapa 1: criar a BlurWorker

No pacote workers, crie uma nova classe Kotlin com o nome BlurWorker.

Etapa 2: adicionar um construtor

Adicione uma dependência a Worker para a classe BlurWorker:

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

Etapa 3: substituir e implementar doWork()

O Worker desfocará a imagem de cupcake exibida.

Para ver melhor quando o trabalho está sendo executado, você usará o makeStatusNotification() do WorkerUtil. Esse método permite que você exiba facilmente um banner de notificação na parte superior da tela.

Substitua o método doWork() e implemente o seguinte. Consulte o código concluído no final da seção:

  1. Acesse um Context chamando a propriedade applicationContext. Atribua-o a um novo val chamado appContext. Ele será necessário para várias manipulações de bitmap que você está prestes a fazer.
  2. Mostre uma notificação de status usando a função makeStatusNotification para notificar o usuário sobre o desfoque da imagem.
  3. Crie um Bitmap com a imagem do cupcake:
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.android_cupcake)
  1. Acesse uma versão desfocada do bitmap chamando o método blurBitmap de WorkerUtils.
  2. Grave esse bitmap em um arquivo temporário chamando o método writeBitmapToFile de WorkerUtils. Salve o URI retornado em uma variável local.
  3. Faça uma notificação exibir o URI chamando o método makeStatusNotification de WorkerUtils.
  4. Retorne o Result.success().
  5. Una o código das etapas 3 a 6 em uma instrução try/catch. Capture um Throwable genérico.
  6. Na declaração de captura, exiba uma mensagem de erro usando a instrução de registro: Log.e(TAG, "Error applying blur").
  7. Na declaração de captura, retorne Result.failure().

O código completo desta etapa é mostrado abaixo.

**BlurWorker.**kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R

private const val TAG = "BlurWorker"
class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

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

        makeStatusNotification("Blurring image", appContext)

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

            val output = blurBitmap(picture, appContext)

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

            makeStatusNotification("Output is $outputUri", appContext)

            Result.success()
        } catch (throwable: Throwable) {
            Log.e(TAG, "Error applying blur")
            Result.failure()
        }
    }
}

Etapa 4: acessar a WorkManager no ViewModel

Crie uma variável de classe para uma instância de WorkManager em ViewModel:

BlurViewModel.kt

private val workManager = WorkManager.getInstance(application)

Etapa 5: colocar uma WorkRequest na fila na WorkManager

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

  • OneTimeWorkRequest: uma WorkRequest que será executada apenas uma vez.
  • PeriodicWorkRequest: uma WorkRequest que será repetida em um ciclo.

Só queremos que a imagem seja desfocada uma vez quando o botão Go for clicado. O método applyBlur é chamado quando o botão Go é clicado, então crie uma OneTimeWorkRequest usando BlurWorker. Em seguida, coloque sua WorkRequest. na fila usando a instância WorkManager.

Adicione a seguinte linha de código ao método applyBlur() BlurViewModel's:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
   workManager.enqueue(OneTimeWorkRequest.from(BlurWorker::class.java))
}

Etapa 6: executar o código

Execute o código. Ele será compilado e você verá a notificação quando pressionar o botão Go. Para ver um resultado mais desfocado, selecione a opção "Mais desfocado" ou "O mais desfocado".

ed497b57e1f527be.png

Para confirmar se a imagem foi desfocada, abra o Device File Explorer no Android Studio:

cf10a1af6e84f5ff.png

Depois, navegue até dados > dados > com.example.background > arquivos > blur_filter_outputs> <URI> e confirme se o cupcake realmente foi desfocado:

e1f61035d680ba03.png

5. Adicionar entrada e saída

Desfocar o recurso de imagem no diretório de recursos é muito bom, mas, para que o Blur-O-Matic seja o app revolucionário de edição de imagens que está destinado a ser, você precisa permitir que o usuário desfoque a imagem que ele vê na tela e, em seguida, mostrar o resultado desfocado.

Para fazer isso, forneceremos o URI da imagem do cupcake exibido como entrada da nossa WorkRequest exibida e usaremos a saída da WorkRequest para exibir a versão final da imagem desfocada.

Etapa 1: criar um objeto de entrada de dados

A entrada e a saída são transmitidas por objetos Data. Objetos Data são contêineres leves para pares de chave-valor. O objetivo deles é armazenar uma quantidade pequena de dados que podem ser transmitidos de/para WorkRequests.

O URI será transmitido da imagem do usuário para um pacote. Ele será armazenado em uma variável chamada imageUri.

Na BlurViewModel, crie um método particular com o nome createInputDataForUri. Esse método vai:

  1. Criar um objeto Data.Builder. Importar androidx.work.Data quando solicitado.
  2. se imageUri for um URI não nulo, adicioná-lo ao objeto Data usando o método putString. Esse método usa uma chave e um valor. Você pode usar a constante KEY_IMAGE_URI de string da classe Constants;
  3. chamar build() no objeto Data.Builder para criar o objeto Data e retorná-lo.

Veja abaixo o método createInputDataForUri completo:

BlurViewModel.kt

/**
 * Creates the input data bundle which includes the Uri to operate on
 * @return Data which contains the Image Uri as a String
 */
private fun createInputDataForUri(): Data {
    val builder = Data.Builder()
    imageUri?.let {
        builder.putString(KEY_IMAGE_URI, imageUri.toString())
    }
    return builder.build()
}

Etapa 2: transmitir o objeto Data para a WorkRequest

Você mudará o método applyBlur em BlurViewModel para que ele faça o seguinte:

  1. Crie um novo OneTimeWorkRequestBuilder.
  2. Chame setInputData, transmitindo o resultado de createInputDataForUri.
  3. Crie a OneTimeWorkRequest.
  4. Coloque a solicitação de trabalho em fila usando a solicitação WorkManager para que o trabalho seja programado para execução.

Veja abaixo o método applyBlur completo:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
            .setInputData(createInputDataForUri())
            .build()

    workManager.enqueue(blurRequest)
}

Etapa 3: atualizar o doWork() do BlurWorker para acessar a entrada

Agora, vamos atualizar o método doWork() do BlurWorker para acessar o URI transmitido do objeto Data:

BlurWorker.kt

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

    // ADD THIS LINE
    val resourceUri = inputData.getString(KEY_IMAGE_URI)
    // ... rest of doWork()
}

Etapa 4: desfocar o URI fornecido

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

  1. Remova o código anterior que estava recebendo o recurso de imagem.

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

  1. Verifique se o resourceUri recebido da Data transmitida não está vazio.
  2. Atribua a variável picture para ser a imagem que foi transmitida da seguinte maneira:

val picture = BitmapFactory.decodeStream(

appContext.contentResolver.

  `openInputStream(Uri.parse(resourceUri)))`

BlurWorker.kt

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

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    return try {
        // REMOVE THIS
        //    val picture = BitmapFactory.decodeResource(
        //            appContext.resources,
        //            R.drawable.android_cupcake)

        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

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

        Result.success()
    } catch (throwable: Throwable) {
        Log.e(TAG, "Error applying blur")
        throwable.printStackTrace()
        Result.failure()
    }
}

Etapa 5: URI temporário de saída

Você terminou esse worker e pode retornar o URI de saída em Result.success(). Forneça o URI de saída como um Data de saída para facilitar o acesso a essa imagem temporária por outros workers para mais operações. Isso será útil no próximo capítulo quando você criar uma cadeia de workers. Para isso, faça o seguinte:

  1. Crie um novo Data, assim como fez com a entrada, e armazene outputUri como uma String. Use a mesma chave (KEY_IMAGE_URI).
  2. Retorne isso à WorkManager usando o método Result.success(Data outputData).

BlurWorker.kt

Modifique a linha Result.success() em doWork() para:

val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

Result.success(outputData)

Etapa 6: executar o app

Agora, você executará o app. Ele será compilado e terá o mesmo comportamento que você pode ver com a imagem desfocada por meio do Device File Explorer, mas ainda não na tela.

Para verificar se há outra imagem desfocada, abra o Device File Explorer no Android Studio e navegue até data/data/com.example.background/files/blur_filter_outputs/<URI>, como você fez na última etapa.

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

7e717ffd6b3d9d52.png

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

6. Encadear seu trabalho

No momento, você está fazendo uma única tarefa: desfocar a imagem. Esse é um ótimo primeiro passo, mas faltam algumas funções básicas:

  • Os arquivos temporários não são limpos.
  • A imagem não é salva em um arquivo permanente.
  • A imagem sempre é desfocada no mesmo nível.

Usaremos uma cadeia de trabalho da WorkManager para adicionar essas funções.

A WorkManager permite que você crie WorkerRequests separadas que são executadas em ordem ou paralelamente. Nesta etapa, você criará uma cadeia de trabalho semelhante a esta:

54832b34e9c9884a.png

As WorkRequests são representadas como caixas.

Outra característica interessante do encadeamento é que a saída de uma WorkRequest se torna a entrada da próxima WorkRequest na cadeia. A entrada e a saída transmitidas entre cada WorkRequest são mostradas como texto azul.

Etapa 1: criar workers de limpeza e salvamento

Primeiro, defina todas as classes de Worker necessárias. Você já tem um Worker para desfocar uma imagem, mas também precisa de um que limpe arquivos temporários e um que salve a imagem permanentemente.

Crie duas novas classes no pacote workers que ampliem Worker.

A primeira será chamada de CleanupWorker e a segunda de SaveImageToFileWorker.

Etapa 2: ampliar o worker

Estenda a classe CleanupWorker da classe Worker. Adicione os parâmetros do construtor necessários.

class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
}

Etapa 3: substituir e implementar doWork() para CleanupWorker

O CleanupWorker não precisa receber nenhuma entrada nem transmitir nenhuma saída. Os arquivos temporários, se houver, serão sempre excluídos. Como a manipulação de arquivos está fora do escopo deste codelab, você pode copiar o código para a CleanupWorker abaixo:

CleanupWorker.kt

package com.example.background.workers

import android.content.Context
import android.util.Log
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File

/**
 * Cleans up temporary files generated during blurring process
 */
private const val TAG = "CleanupWorker"
class CleanupWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

    override 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("Cleaning up old temporary files", applicationContext)
        sleep()

        return 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) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

Etapa 4: substituir e implementar doWork() para SaveImageToFileWorker

O SaveImageToFileWorker receberá entrada e saída. A entrada é um String do URI de imagem temporariamente desfocado, armazenado com a chave KEY_IMAGE_URI. A saída também será uma String, o URI da imagem desfocada salva armazenada com a chave KEY_IMAGE_URI.

4fc29ac70fbecf85.png

Como este ainda não é um codelab sobre manipulação de arquivos, o código é fornecido abaixo. Os valores resourceUri e output são recuperados com a chave KEY_IMAGE_URI. Ele é muito parecido com o código que você programou na última etapa para entrada e saída, já que usa as mesmas chaves.

SaveImageToFileWorker.kt

package com.example.background.workers

import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import androidx.work.workDataOf
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.KEY_IMAGE_URI
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

/**
 * Saves the image to a permanent file
 */
private const val TAG = "SaveImageToFileWorker"
class SaveImageToFileWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

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

    override 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("Saving image", applicationContext)
        sleep()

        val resolver = applicationContext.contentResolver
        return 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, "Writing to MediaStore failed")
                Result.failure()
            }
        } catch (exception: Exception) {
            exception.printStackTrace()
            Result.failure()
        }
    }
}

Etapa 5: modificar a notificação do BlurWorker

Agora que temos uma cadeia de Workers para salvar a imagem na pasta correta, podemos desacelerar o trabalho usando o método sleep() definido na classe WorkerUtils para facilitar o processo para ver cada WorkRequest iniciada, mesmo em dispositivos emulados. A versão final de BlurWorker fica assim:

BlurWorker.kt

class BlurWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {

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

    val resourceUri = inputData.getString(KEY_IMAGE_URI)

    makeStatusNotification("Blurring image", appContext)

    // ADD THIS TO SLOW DOWN THE WORKER
    sleep()
    // ^^^^

    return try {
        if (TextUtils.isEmpty(resourceUri)) {
            Timber.e("Invalid input uri")
            throw IllegalArgumentException("Invalid input uri")
        }

        val resolver = appContext.contentResolver

        val picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)))

        val output = blurBitmap(picture, appContext)

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

        val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())

        Result.success(outputData)
    } catch (throwable: Throwable) {
        throwable.printStackTrace()
        Result.failure()
    }
}

Etapa 6: criar uma cadeia de WorkRequest

Você precisa modificar o método applyBlur do BlurViewModel para executar uma cadeia de WorkRequests em vez de apenas uma delas. Atualmente, o código é assim:

BlurViewModel.kt

val blurRequest = OneTimeWorkRequestBuilder<BlurWorker>()
        .setInputData(createInputDataForUri())
        .build()

workManager.enqueue(blurRequest)

Em vez de chamar workManager.enqueue(), chame workManager.beginWith(). Isso retorna uma WorkContinuation, que define uma cadeia de WorkRequests. Você pode adicionar itens a essa cadeia de solicitações de trabalho chamando o método then(). Por exemplo, se tiver três objetos WorkRequest (workA, workB e workC), você pode fazer o seguinte:

// Example code, don't copy to the project
val continuation = workManager.beginWith(workA)

continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
        .then(workC)
        .enqueue() // Enqueues the WorkContinuation which is a chain of work

Isso produziria e executaria a seguinte cadeia de WorkRequests:

bf3b82eb9fd22349.png

Crie uma cadeia com uma WorkRequest CleanupWorker, uma BlurImage WorkRequest e uma SaveImageToFile WorkRequest em applyBlur. Transmita a entrada para a BlurImage WorkRequest.

O código resultante será o seguinte:

BlurViewModel.kt

internal 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 blurRequest = OneTimeWorkRequest.Builder(BlurWorker::class.java)
            .setInputData(createInputDataForUri())
            .build()

    continuation = continuation.then(blurRequest)

    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequest.Builder(SaveImageToFileWorker::class.java).build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

Ele será compilado e executado. Agora, você pode pressionar o botão Go e ver as notificações quando os diferentes workers estiverem sendo executados. Você ainda verá a imagem desfocada no Device File Explorer e, em uma próxima etapa, você adicionará um botão extra para que os usuários possam ver a imagem desfocada no dispositivo.

Nas capturas de tela abaixo, as mensagens de notificação exibem qual worker está em execução.

f0bbaf643c24488f.png 42a036f4b24adddb.png

a438421064c385d4.png

Etapa 7: repetir o BlurWorker

Está na hora de adicionar um recurso para desfocar a imagem em níveis diferentes. Pegue o parâmetro blurLevel transmitido para applyBlur e adicione essa quantidade de operações WorkRequest de desfoque à cadeia. Apenas a primeira WorkRequest precisa receber a entrada do URI.

Faça o teste e compare com o código abaixo:

BlurViewModel.kt

internal fun applyBlur(blurLevel: Int) {
    // Add WorkRequest to Cleanup temporary images
    var continuation = workManager
            .beginWith(OneTimeWorkRequest
            .from(CleanupWorker::class.java))

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

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

    // Add WorkRequest to save the image to the filesystem
    val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
            .build()

    continuation = continuation.then(save)

    // Actually start the work
    continuation.enqueue()
}

Abra o Device File Explorer para ver as imagens desfocadas. Observe que a pasta de saída contém várias imagens desfocadas, imagens que estão nos estágios intermediários de desfoque e a imagem final exibindo a imagem desfocada de acordo com o valor de desfoque selecionado.

Ótimo trabalho! Agora você pode desfocar uma imagem o quanto quiser. Quanto mistério!

7. Garantir um trabalho único

Agora que você já usou as cadeias, está na hora de abordar outro recurso poderoso da WorkManager: cadeias de trabalho únicas.

Às vezes, você quer que apenas uma cadeia de trabalho seja executada por vez. Por exemplo, talvez você tenha uma cadeia de trabalho que sincroniza os dados locais com o servidor, então é recomendável deixar a primeira sincronização de dados terminar antes de iniciar uma nova. Para fazer isso, use beginUniqueWork em vez de beginWith e forneça um nome de String exclusivo. Isso nomeia toda a cadeia de solicitações de trabalho para que você possa consultá-las em conjunto.

Use beginUniqueWork para garantir que a cadeia de trabalho para desfoque do arquivo seja única. Transmita IMAGE_MANIPULATION_WORK_NAME como a chave. Você também precisa transmitir uma ExistingWorkPolicy. Suas opções são REPLACE, KEEP ou APPEND.

Você usará REPLACE porque, se o usuário decidir desfocar outra imagem antes que a atual seja concluída, precisamos interromper o processo atual e começar a desfocar a nova imagem.

O código para iniciar a continuação de trabalho único é mostrado abaixo:

BlurViewModel.kt

// REPLACE THIS CODE:
// var continuation = workManager
//            .beginWith(OneTimeWorkRequest
//            .from(CleanupWorker::class.java))
// WITH
var continuation = workManager
        .beginUniqueWork(
                IMAGE_MANIPULATION_WORK_NAME,
                ExistingWorkPolicy.REPLACE,
                OneTimeWorkRequest.from(CleanupWorker::class.java)
        )

O Blur-O-Matic agora desfocará apenas uma imagem por vez.

8. Incluir uma tag e exibir o status de trabalho

Esta seção usa muito LiveData. Por isso, você precisa conhecê-lo para entender totalmente o que está acontecendo. O LiveData é um armazenador de dados observáveis com reconhecimento de ciclo de vida.

Consulte a documentação ou o codelab Componentes compatíveis com ciclo de vida do Android se esta for a primeira vez que você trabalha com o LiveData ou os observáveis.

A próxima grande mudança que você fará é mudar o que é exibido no app durante a execução do trabalho.

Você pode ver o status de qualquer WorkRequest acessando um LiveData que tenha um objeto WorkInfo. WorkInfo é um objeto que contém detalhes sobre o estado atual de uma WorkRequest, incluindo:

A tabela a seguir mostra três maneiras diferentes de acessar objetos LiveData<WorkInfo> ou LiveData<List<WorkInfo>> e o que cada uma faz.

Tipo

Método da WorkManager

Descrição

Acessar o trabalho usando um ID

getWorkInfoByIdLiveData

Cada WorkRequest tem um ID exclusivo gerado pela WorkManager. É possível usá-lo para acessar um único LiveData
para essa WorkRequest específica.

Acessar o trabalho usando um nome da cadeia única

getWorkInfosForUniqueWorkLiveData

Como você acabou de ver, as WorkRequests podem fazer parte de uma cadeia única. Isso retorna LiveData
>
para todo o trabalho em uma cadeia única de WorkRequests.

Acessar o trabalho usando uma tag

getWorkInfosByTagLiveData

Por fim, você pode incluir uma tag em qualquer WorkRequest com uma string. Inclua a mesma tag em várias WorkRequests para associá-las. Isso retorna o LiveData
>
para qualquer tag única.

Você incluirá uma tag na WorkRequest SaveImageToFileWorker para poder acessá-la usando getWorkInfosByTag. Você usará uma tag para identificar o trabalho em vez de usar o ID da WorkManager porque, se o usuário desfocar várias imagens, todas as WorkRequests de salvamento de imagem terão a mesma tag, mas não o mesmo ID. Também é possível escolher a tag.

Você não usaria getWorkInfosForUniqueWork, porque isso retornaria WorkInfo para todas as WorkRequests de desfoque e a WorkRequest de limpeza também. Seria necessário usar uma lógica extra para encontrar a WorkRequest de salvamento de imagem.

Etapa 1: incluir uma tag no trabalho

Em applyBlur, ao criar o SaveImageToFileWorker, inclua uma tag no trabalho usando a constante TAG_OUTPUT de String:

BlurViewModel.kt

val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .addTag(TAG_OUTPUT) // <-- ADD THIS
        .build()

Etapa 2: acessar o WorkInfo

Agora que você incluiu uma tag no trabalho, é possível acessar o WorkInfo:

  1. Em BlurViewModel, declare uma nova variável de classe com o nome outputWorkInfos que é um LiveData<List<WorkInfo>>.
  2. No BlurViewModel, adicione um bloco init para acessar o WorkInfo usando WorkManager.getWorkInfosByTagLiveData.

O código necessário é o seguinte:

BlurViewModel.kt

// New instance variable for the WorkInfo
internal val outputWorkInfos: LiveData<List<WorkInfo>>

// Modify the existing init block in the BlurViewModel class to this:
init {
    imageUri = getImageUri(application.applicationContext)
    // This transformation makes sure that whenever the current work Id changes the WorkInfo
    // the UI is listening to changes
    outputWorkInfos = workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
}

Etapa 3: exibir o WorkInfo

Agora que você tem um LiveData para seu WorkInfo, é possível observá-lo na BlurActivity. No observador:

  1. Confira se a lista de WorkInfo não é nula e se ela tem objetos WorkInfo. Se não tiver, isso significa que o botão Go ainda não foi clicado, então retorne.
  2. Acesse o primeiro WorkInfo na lista. Haverá somente um WorkInfo marcado com TAG_OUTPUT porque tornamos a cadeia de trabalho única.
  3. Use workInfo.state.isFinished para conferir se o trabalho tem um status concluído.
  4. Se não estiver concluído, chame showWorkInProgress(), que oculta o botão Go e mostra o botão Cancel Work e a barra de progresso.
  5. Se estiver concluído, chame showWorkFinished(), que oculta o botão Cancel Work e a barra de progresso e exibe o botão Go.

O código fica assim:

Observação: importe androidx.lifecycle.Observer quando solicitado.

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
    ...
    // Observe work status, added in onCreate()
    viewModel.outputWorkInfos.observe(this, workInfosObserver())
}

// Define the observer function
private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()
        } else {
            showWorkInProgress()
        }
    }
}

Etapa 4: executar o app

Execute o app. Ele será compilado e executado e agora mostrará uma barra de progresso quando estiver funcionando, assim como o botão de cancelamento:

7b70288f69050f0b.png

9. Mostrar a saída final

Cada WorkInfo também tem um método getOutputData, que permite acessar o objeto Data de saída com a imagem salva final. No Kotlin, você pode acessar esse método usando uma variável que a linguagem gera para você: outputData. Exibiremos um botão com a mensagem See File sempre que uma imagem desfocada estiver pronta para exibição.

Etapa 1: criar o botão "See File"

Já existe um botão no layout activity_blur.xml que está oculto. Ele está na BlurActivity e se chama outputButton.

Em BlurActivity, dentro de onCreate(), configure o listener de clique desse botão. Ele precisa acessar o URI e abrir uma atividade para vê-lo. Use o código abaixo:

BlurActivity.kt

override fun onCreate(savedInstanceState: Bundle?) {
   // Setup view output image file button
   binding.seeFileButton.setOnClickListener {
       viewModel.outputUri?.let { currentUri ->
           val actionView = Intent(Intent.ACTION_VIEW, currentUri)
           actionView.resolveActivity(packageManager)?.run {
               startActivity(actionView)
           }
       }
   }
}

Etapa 2: definir o URI e mostrar o botão

Há alguns ajustes finais que você precisa aplicar ao observador de WorkInfo para que isso funcione:

  1. Se o WorkInfo for concluído, acesse os dados de saída usando workInfo.outputData.
  2. Em seguida, acesse o URI de saída. Lembre-se de que ele está armazenado com a chave Constants.KEY_IMAGE_URI.
  3. Se o URI não estiver vazio, ele foi salvo corretamente. Mostre outputButton e chame setOutputUri no modelo de visualização com o URI.

BlurActivity.kt

private fun workInfosObserver(): Observer<List<WorkInfo>> {
    return Observer { listOfWorkInfo ->

        // Note that these next few lines grab a single WorkInfo if it exists
        // This code could be in a Transformation in the ViewModel; they are included here
        // so that the entire process of displaying a WorkInfo is in one location.

        // If there are no matching work info, do nothing
        if (listOfWorkInfo.isNullOrEmpty()) {
            return@Observer
        }

        // We only care about the one output status.
        // Every continuation has only one worker tagged TAG_OUTPUT
        val workInfo = listOfWorkInfo[0]

        if (workInfo.state.isFinished) {
            showWorkFinished()

            // Normally this processing, which is not directly related to drawing views on
            // screen would be in the ViewModel. For simplicity we are keeping it here.
            val outputImageUri = workInfo.outputData.getString(KEY_IMAGE_URI)

            // If there is an output file show "See File" button
            if (!outputImageUri.isNullOrEmpty()) {
                viewModel.setOutputUri(outputImageUri)
                binding.seeFileButton.visibility = View.VISIBLE
            }
        } else {
            showWorkInProgress()
        }
    }
}

Etapa 3: executar o código

Execute o código. Você verá o novo botão clicável See File, que leva ao arquivo gerado:

5366222d0b4fb705.png

cd1ecc8b4ca86748.png

10. Cancelar o trabalho

bc1dc9414fe2326e.png

Você incluiu o botão Cancel Work, então vamos adicionar o código para que ele faça algo. Com a WorkManager, é possível cancelar trabalhos usando o ID, por tag e por nome de cadeia única.

Neste caso, é recomendável cancelar o trabalho por nome de cadeia única, já que você quer cancelar todo o trabalho na cadeia, não apenas uma etapa específica.

Etapa 1: cancelar o trabalho por nome

Em BlurViewModel, adicione um novo método chamado cancelWork() para cancelar o trabalho único. Dentro da chamada de função cancelUniqueWork em workManager, transmita a tag IMAGE_MANIPULATION_WORK_NAME.

BlurViewModel.kt

internal fun cancelWork() {
    workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}

Etapa 2: chamar o método de cancelamento

Em seguida, vincule o botão cancelButton para chamar cancelWork:

BlurActivity.kt

// In onCreate()
// Hookup the Cancel button
binding.cancelButton.setOnClickListener { viewModel.cancelWork() }

Etapa 3: executar e cancelar o trabalho

Execute o app. Ele deve ser compilado sem problemas. Comece a desfocar uma imagem e, em seguida, clique no botão de cancelamento. A cadeia inteira será cancelada.

dcb4ccfd261957b1.png

Agora há apenas o botão GO quando o trabalho é cancelado, porque o WorkState não está mais no estado CONCLUÍDO.

11. Restrições de trabalho

Por último, mas não menos importante, WorkManager é compatível com Constraints. Para o Blur-O-Matic, você usará a restrição que o dispositivo precisa estar carregando. Isso significa que a solicitação de trabalho só será executada se o dispositivo estiver carregando.

Etapa 1: criar e adicionar uma restrição de carregamento

Para criar um objeto Constraints, use um Constraints.Builder. Em seguida, defina as restrições que você quer e as adicione à WorkRequest usando o método setRequiresCharging(), conforme mostrado abaixo:

Importe androidx.work.Constraints quando solicitado.

BlurViewModel.kt

// Put this inside the applyBlur() function, above the save work request.
// Create charging constraint
val constraints = Constraints.Builder()
        .setRequiresCharging(true)
        .build()

// Add WorkRequest to save the image to the filesystem
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
        .setConstraints(constraints)
        .addTag(TAG_OUTPUT)
        .build()
continuation = continuation.then(save)

// Actually start the work
continuation.enqueue()

Etapa 2: testar com o emulador ou o dispositivo

Agora você pode executar o Blur-O-Matic. Se você está usando um dispositivo, pode removê-lo ou conectá-lo à fonte de energia. Em um emulador, você pode mudar o status de carregamento na janela Extended controls:

406ce044ca07169f.png

Quando o dispositivo não estiver carregando, ele precisará suspender o SaveImageToFileWorker, executando-o apenas depois que você conectá-lo a uma fonte de energia.

302da5ec986ae769.png

12. Parabéns

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;
  • nomear cadeias WorkRequest únicas;
  • incluir tags em WorkRequests;
  • exibir WorkInfo na IU;
  • cancelar WorkRequests;
  • adicionar restrições a uma WorkRequest.

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

Ou, se preferir, você pode clonar o codelab da WorkManager no GitHub:

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

A 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 da WorkManager ou prossiga para o codelab da WorkManager avançada.