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á.
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
- A versão estável mais recente do Android Studio.
- Você também precisa conhecer o
LiveData
e oViewModel
. Caso você ainda não conheça essas classes, confira o codelab Componentes compatíveis com ciclo de vida do Android (especificamente para ViewModel e LiveData) ou o codelab Room com visualização (uma introdução aos componentes de arquitetura).
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:
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 exibirNotifications
, 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 exibirBlurActivity
. 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 deBlurActivity
.
***** 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étododoWork()
.WorkRequest
: representa uma solicitação para realizar algum trabalho. Você transmitirá oWorker
como parte da criação daWorkRequest
. Ao criar aWorkRequest
, você também pode especificar itens comoConstraints
ou quando oWorker
deve ser executado.WorkManager
: essa classe programa suasWorkRequest
e as executa. Ela programaWorkRequest
s 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:
- Acesse um
Context
chamando a propriedadeapplicationContext
. Atribua-o a um novoval
chamadoappContext
. Ele será necessário para várias manipulações de bitmap que você está prestes a fazer. - Mostre uma notificação de status usando a função
makeStatusNotification
para notificar o usuário sobre o desfoque da imagem. - Crie um
Bitmap
com a imagem do cupcake:
val picture = BitmapFactory.decodeResource(
appContext.resources,
R.drawable.android_cupcake)
- Acesse uma versão desfocada do bitmap chamando o método
blurBitmap
deWorkerUtils
. - Grave esse bitmap em um arquivo temporário chamando o método
writeBitmapToFile
deWorkerUtils
. Salve o URI retornado em uma variável local. - Faça uma notificação exibir o URI chamando o método
makeStatusNotification
deWorkerUtils
. - Retorne o
Result.success()
. - Una o código das etapas 3 a 6 em uma instrução try/catch. Capture um
Throwable
genérico. - Na declaração de captura, exiba uma mensagem de erro usando a instrução de registro:
Log.e(TAG, "Error applying blur")
. - 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 WorkRequest
s:
OneTimeWorkRequest
: umaWorkRequest
que será executada apenas uma vez.PeriodicWorkRequest
: umaWorkRequest
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".
Para confirmar se a imagem foi desfocada, abra o Device File Explorer no Android Studio:
Depois, navegue até dados > dados > com.example.background > arquivos > blur_filter_outputs> <URI> e confirme se o cupcake realmente foi desfocado:
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 WorkRequest
s.
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:
- Criar um objeto
Data.Builder
. Importarandroidx.work.Data
quando solicitado. - se
imageUri
for umURI
não nulo, adicioná-lo ao objetoData
usando o métodoputString
. Esse método usa uma chave e um valor. Você pode usar a constanteKEY_IMAGE_URI
de string da classeConstants
; - chamar
build()
no objetoData.Builder
para criar o objetoData
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:
- Crie um novo
OneTimeWorkRequestBuilder
. - Chame
setInputData
, transmitindo o resultado decreateInputDataForUri
. - Crie a
OneTimeWorkRequest
. - 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.
- Remova o código anterior que estava recebendo o recurso de imagem.
val picture = BitmapFactory.decodeResource(appContext.
resources
, R.drawable.
android_cupcake
)
- Verifique se o
resourceUri
recebido daData
transmitida não está vazio. - 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:
- Crie um novo
Data
, assim como fez com a entrada, e armazeneoutputUri
como umaString
. Use a mesma chave (KEY_IMAGE_URI
). - 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:
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 WorkerRequest
s separadas que são executadas em ordem ou paralelamente. Nesta etapa, você criará uma cadeia de trabalho semelhante a esta:
As WorkRequest
s 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
.
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 Worker
s 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 WorkRequest
s 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 WorkRequest
s. 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. 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.
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:
- se o trabalho é
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
ouSUCCEEDED
. - se a
WorkRequest
for concluída, todos os dados de saída do trabalho.
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 |
| Cada |
Acessar o trabalho usando um nome da cadeia única |
| Como você acabou de ver, as |
Acessar o trabalho usando uma tag |
| Por fim, você pode incluir uma tag em qualquer WorkRequest com uma string. Inclua a mesma tag em várias |
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 WorkRequest
s 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 WorkRequest
s 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
:
- Em
BlurViewModel
, declare uma nova variável de classe com o nomeoutputWorkInfos
que é umLiveData<List<WorkInfo>>
. - No
BlurViewModel
, adicione um bloco init para acessar oWorkInfo
usandoWorkManager.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:
- Confira se a lista de
WorkInfo
não é nula e se ela tem objetosWorkInfo
. Se não tiver, isso significa que o botão Go ainda não foi clicado, então retorne. - Acesse o primeiro
WorkInfo
na lista. Haverá somente umWorkInfo
marcado comTAG_OUTPUT
porque tornamos a cadeia de trabalho única. - Use
workInfo.state.isFinished
para conferir se o trabalho tem um status concluído. - 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. - 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:
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:
- Se o
WorkInfo
for concluído, acesse os dados de saída usandoworkInfo.outputData
. - Em seguida, acesse o URI de saída. Lembre-se de que ele está armazenado com a chave
Constants.KEY_IMAGE_URI
. - Se o URI não estiver vazio, ele foi salvo corretamente. Mostre
outputButton
e chamesetOutputUri
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:
10. Cancelar o trabalho
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.
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:
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.
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
WorkRequest
s; - nomear cadeias
WorkRequest
únicas; - incluir tags em
WorkRequest
s; - exibir
WorkInfo
na IU; - cancelar
WorkRequest
s; - 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.