Trabalho em segundo plano com WorkManager (Kotlin)

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
  • Gerencia a compatibilidade de nível da API de volta até o nível 14. Consulte a observação
  • Funciona com ou sem o Google Play Services
  • Segue 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á.

Foto de um robalo-riscado de Peggy Greb, Serviço de Pesquisa Agrícola do Departamento Agrícola dos EUA (USDA, na sigla em inglês).

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

Se você não entender algum ponto…

Se você não entender algum ponto deste codelab ou quiser olhar o estado final do código, use o link a seguir:

Download do código final

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

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

Etapa 1: fazer o download do código

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

Download do código inicial

Ou, se preferir, clone o codelab de navegação no GitHub:

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

Etapa 2: obter uma imagem

Se você está usando um dispositivo no qual já fez o download de imagens ou tirou fotos, podemos começar.

Se está usando um dispositivo novo (como um emulador recém-criado), tire uma foto ou faça o download de uma imagem na Web com ele. Escolha algo misterioso.

Etapa 3: executar o app

Execute o app. As telas a seguir serão exibidas. Conceda as permissões de acesso às fotos na solicitação inicial e, se a imagem for desativada, reabra o app:


Você pode selecionar uma imagem e avançar para a próxima tela, que tem 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 o seguinte:

  • BlurApplication: essa classe contém a configuração do app.
  • WorkerUtils: essa classe contém o código para desfoque e alguns métodos práticos que você usará posteriormente para exibir Notifications 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 a BlurActivity. Ele 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.
  • SelectImageActivity: a primeira atividade que permite selecionar uma imagem.
  • res/activity_blur.xml e res/activity_select.xml: os arquivos de layout de cada atividade.

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

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

app/build.gradle

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

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

build.gradle

versions.work = "2.3.4"

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

Nesta etapa, você usará uma imagem na pasta res/drawable chamada test.jpg 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 chamada 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 res/test.jpg.

Substitua o método doWork() e implemente o seguinte:

  1. Acesse um Context chamando a propriedade applicationContext. Ele será necessário para várias manipulações de bitmap que você está prestes a fazer.
  2. Crie um Bitmap com a imagem de teste:
val picture = BitmapFactory.decodeResource(
        appContext.resources,
        R.drawable.test)
  1. Acesse uma versão desfocada do bitmap chamando o método estático blurBitmap de WorkerUtils.
  2. Grave esse bitmap em um arquivo temporário chamando o método estático 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 estático makeStatusNotification de WorkerUtils.
  4. Retorne Result.success().
  5. Una o código das etapas 2 a 6 em uma instrução try/catch. Capture um Throwable genérico.
  6. Na declaração de captura, emita um erro log statement: Timber.e(throwable, "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 androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.R
import timber.log.Timber

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.test)

            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) {
            Timber.e(throwable, "Error applying blur")
            Result.failure()
        }
    }
}

Etapa 4: acessar a WorkManager no ViewModel

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

BlurViewModel.kt

private val workManager = WorkManager.getInstance(application)

Etapa 5: colocar uma WorkRequest na fila na WorkManager

Muito bem. Agora vamos fazer uma 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 da WorkManager.

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

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.


Você também pode abrir o Device File Explorer no Android Studio:

Depois, navegue até data>data>com.example.background>files>blur_filter_outputs><URI> e confirme se o peixe foi desfocado:


Desfocar a imagem de teste é 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 os usuários desfoquem as próprias imagens.

Para fazer isso, forneceremos o URI da imagem do usuário selecionada como entrada da nossa WorkRequest.

Etapa 1: criar um objeto de entrada Data

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.

Crie um método particular chamado createInputDataForUri. Esse método vai:

  1. criar um objeto Data.Builder;
  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

Mude o método applyBlur para que ele faça o seguinte:

  1. Crie um novo OneTimeWorkRequest.Builder.
  2. Chame setInputData, transmitindo o resultado de createInputDataForUri.
  3. Crie a OneTimeWorkRequest.
  4. Coloque essa solicitação na fila usando a WorkManager.

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, é possível desfocar a imagem que o usuário selecionou:

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.test)

        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)

        Result.success()
    } catch (throwable: Throwable) {
        Timber.e(throwable)
        Result.failure()
    }
}

Etapa 5: URI temporário de saída

Terminamos esse worker e agora podemos retornar Result.success(). Forneceremos o OutputURI como 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 criarmos uma cadeia de workers. Para fazer isso:

  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 precisa ser compilado e ter o mesmo comportamento.

Você também pode abrir o Device File Explorer no Android Studio e navegar até data/data/com.example.background/files/blur_filter_outputs/<URI>, como fez na última etapa.

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

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

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:

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 worker que ampliem Worker.

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

Etapa 2: ampliar o worker

Adicione uma dependência ao Worker para a classe CleanupWorker:

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

Etapa 3: substituir e implementar doWork() para CleanerWorker

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 este não é um codelab sobre manipulação de arquivos, você pode copiar o código para o CleanupWorker abaixo:

CleanupWorker.kt

package com.example.background.workers

import android.content.Context
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.example.background.OUTPUT_PATH
import java.io.File
import timber.log.Timber

/**
 * Cleans up temporary files generated during blurring process
 */
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()
                            Timber.i("Deleted $name - $deleted")
                        }
                    }
                }
            }
            Result.success()
        } catch (exception: Exception) {
            Timber.e(exception)
            Result.failure()
        }
    }
} 

Etapa 4: substituir e implementar doWork() para SaveImageToFileWorker

O SaveImageToFileWorker receberá entrada e saída. A entrada é uma String armazenada com a chave KEY_IMAGE_URI. A saída também será uma String armazenada com a chave KEY_IMAGE_URI.

Como este ainda não é um codelab sobre manipulação de arquivos, o código é apresentado abaixo, com dois TODOs para você preencher o código adequado para entrada e saída. 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 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
import timber.log.Timber

/**
 * Saves the image to a permanent file
 */
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 {
                Timber.e("Writing to MediaStore failed")
                Result.failure()
            }
        } catch (exception: Exception) {
            Timber.e(exception)
            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 para facilitar a visualização do início de cada WorkRequest, mesmo em dispositivos emulados. A versão final do 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) {
        Timber.e(throwable)
        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:

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. A imagem que você escolheu desfocar será salva na pasta Imagens:

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

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

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.

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<WorkInfo> 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<List<WorkInfo>> 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<List<WorkInfos>> 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. Declare uma nova variável chamada 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>>

// Add an init block to the BlurViewModel class
init {
    // 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 e mostra as visualizações adequadas.
  5. Se estiver concluído, chame showWorkFinished(), que oculta e mostra as visualizações adequadas.

O código fica assim:

BlurActivity.kt

// Show work status, added in onCreate()
viewModel.outputWorkInfos.observe(this, workInfosObserver())

// Add this functions
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:

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 o acessador sintético gerado pela linguagem: 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.

Configure o listener de clique para esse botão. Ele precisa acessar o URI e abrir uma atividade para vê-lo. Use o código abaixo:

BlurActivity.kt

// Put this inside onCreate()
// 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 o 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 as String)
                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:

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

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.

Por último, mas não menos importante, a WorkManager é compatível com Constraints. Para o Blur-O-Matic, você usará a restrição de que o dispositivo precisa estar sendo carregado durante o salvamento.

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, conforme mostrado abaixo:

BlurViewModel.kt

// Put this inside the applyBlur() function
// 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:

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.

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

  • adicionar a WorkManager ao projeto;
  • programar uma OneOffWorkRequest;
  • 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:

Download do código final

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 WorkManager avançada.