1. Introdução
No codelab Trabalho em segundo plano com o WorkManager, você aprendeu a executar trabalhos em segundo plano (não na linha de execução principal) usando o WorkManager. Neste codelab, você vai continuar aprendendo sobre a funcionalidade do WorkManager para garantir trabalho exclusivo, inclusão de tags, cancelamento e restrições de trabalho. E também vamos abordar como criar testes automatizados para verificar se os workers funcionam corretamente e retornam os resultados esperados. Vamos mostrar também como usar o Inspetor de tarefas em segundo plano, fornecido pelo Android Studio, para inspecionar workers em fila.
O que você vai criar
Neste codelab, você vai garantir o trabalho exclusivo, a inclusão de tags, o cancelamento e a implementação de restrições. Em seguida, você vai aprender a criar testes de interface automatizados no app Blur-O-Matic que verificam a funcionalidade dos três workers criados no codelab Trabalho em segundo plano com o WorkManager:
BlurWorker
CleanupWorker
SaveImageToFileWorker
O que você vai aprender
- Como garantir um trabalho exclusivo.
- Como cancelar um trabalho.
- Como definir restrições de trabalho.
- Como criar testes automatizados para verificar a funcionalidade do worker.
- Os conceitos básicos da inspeção de workers na fila com o Inspetor de tarefas em segundo plano.
O que é necessário
- A versão estável mais recente do Android Studio
- Já ter concluído o codelab Trabalho em segundo plano com o WorkManager.
- Um dispositivo ou emulador Android
2. Etapas da configuração
Fazer o download do código
Clique no link abaixo para fazer o download de todo o código para este codelab:
Se preferir, clone o código no GitHub:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git $ cd basic-android-kotlin-compose-training-workmanager $ git checkout intermediate
Abra o projeto no Android Studio.
3. Garantir trabalho exclusivo
Agora que você sabe como encadear workers, é hora de usar outro recurso poderoso do WorkManager: sequências 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 seus dados locais com o servidor. É provável que você queira que a primeira sincronização de dados seja concluída antes de iniciar uma nova. Para fazer isso, use beginUniqueWork()
em vez de beginWith()
e forneça um nome de String
exclusivo. Esse nome é usado em toda a cadeia de solicitações de trabalho para que você as possa consultar em conjunto.
Também é necessário transmitir um objeto ExistingWorkPolicy
. Esse objeto informa ao SO Android o que acontece se o trabalho já existe. Os valores possíveis para ExistingWorkPolicy
são REPLACE
, KEEP
, APPEND
ou APPEND_OR_REPLACE
.
Neste app, você quer usar REPLACE
porque, se um usuário decidir desfocar outra imagem antes de terminar a atual, você precisa interromper o desfoque da imagem atual e começar a desfocar a nova.
Também é importante garantir que, se um usuário clica em Start quando uma solicitação de trabalho já está na fila, o app vai substituir a solicitação de trabalho anterior pela nova. Não faz sentido continuar trabalhando na solicitação anterior, porque o app a substitui pela nova.
No arquivo data/WorkManagerBluromaticRepository.kt
, no método applyBlur()
, siga estas etapas:
- Remova a chamada para a função
beginWith()
e adicione uma chamada à funçãobeginUniqueWork()
. - No primeiro parâmetro para a função
beginUniqueWork()
, transmita a constanteIMAGE_MANIPULATION_WORK_NAME
. - No segundo parâmetro,
existingWorkPolicy
, transmitaExistingWorkPolicy.REPLACE
. - No terceiro parâmetro, crie uma nova
OneTimeWorkRequest
para oCleanupWorker
.
data/WorkManagerBluromaticRepository.kt
import androidx.work.ExistingWorkPolicy
import com.example.bluromatic.IMAGE_MANIPULATION_WORK_NAME
...
// 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 desfoca apenas uma imagem por vez.
4. Marcar e atualizar a interface com base no status do trabalho
A próxima mudança que você vai fazer é sobre o que o app mostra quando o trabalho é executado. As informações retornadas sobre os trabalhos na fila determinam como a interface precisa mudar.
Esta tabela mostra três métodos diferentes para chamar informações de trabalho:
Tipo | Método da WorkManager | Descrição |
Acessar o trabalho usando um ID | Essa função retorna um único LiveData<WorkInfo> para uma WorkRequest específica usando o ID. | |
Acessar o trabalho usando um nome da cadeia única | Essa função retorna LiveData<List<WorkInfo>> para todo o trabalho em uma cadeia exclusiva de WorkRequests. | |
Acessar o trabalho usando uma tag | Essa função retorna a LiveData<List<WorkInfo>> de uma tag. |
Um objeto WorkInfo
contém detalhes sobre o estado atual de uma WorkRequest
, incluindo:
- Se o trabalho está no estado
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
ouSUCCEEDED
; - Se a
WorkRequest
está concluída e se há dados de saída do trabalho.
Esses métodos retornam LiveData. LiveData é um detentor de dados observáveis com reconhecimento de ciclo de vida. Convertemos esse elemento em um fluxo de objetos WorkInfo
chamando .asFlow()
.
Como você tem interesse em saber quando a imagem final é salva, adicione uma tag à WorkRequest de SaveImageToFileWorker
para receber WorkInfo do método getWorkInfosByTagLiveData()
.
Outra opção é usar o método getWorkInfosForUniqueWorkLiveData()
, que retorna informações sobre as três WorkRequests (CleanupWorker
, BlurWorker
e SaveImageToFileWorker
). A desvantagem desse método é que você precisa de outro código para encontrar especificamente as informações necessárias da SaveImageToFileWorker
.
Marcar a solicitação de trabalho
A inclusão de tags no trabalho é feita no arquivo data/WorkManagerBluromaticRepository.kt
, na função applyBlur()
.
- Ao criar a solicitação de trabalho
SaveImageToFileWorker
, marque o trabalho chamando o métodoaddTag()
e transmitindo aTAG_OUTPUT
da constanteString
.
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.TAG_OUTPUT
...
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.addTag(TAG_OUTPUT) // <- Add this
.build()
Em vez de um ID do WorkManager, use uma tag para rotular o trabalho porque, se o usuário desfocar várias imagens, todas as WorkRequest
s das imagens salvas vão ter a mesma tag, mas não o mesmo ID.
Acessar o WorkInfo
Use as informações do objeto WorkInfo
da solicitação de trabalho SaveImageToFileWorker
na lógica para decidir quais elementos combináveis precisam ser mostrados na interface com base no BlurUiState
.
O ViewModel consome essas informações da variável outputWorkInfo
do repositório.
Agora que você marcou a solicitação de trabalho SaveImageToFileWorker
, siga estas etapas para extrair as informações:
- No arquivo
data/WorkManagerBluromaticRepository.kt
, chame o métodoworkManager.getWorkInfosByTagLiveData()
para preencher a variáveloutputWorkInfo
. - Transmita a constante
TAG_OUTPUT
ao parâmetro do método.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
...
A chamada do método getWorkInfosByTagLiveData()
retorna LiveData. LiveData é um detentor de dados observáveis com reconhecimento de ciclo de vida. A função .asFlow()
o converte em um fluxo.
- Encadeie uma chamada para a função
.asFlow()
para converter o método em um fluxo. Converta o método para que o app funcione com um fluxo Kotlin em vez do LiveData.
data/WorkManagerBluromaticRepository.kt
import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
- Encadeie uma chamada para a função de transformação
.mapNotNull()
(link em inglês) para garantir que o fluxo contenha valores. - Para a regra de transformação, se o elemento não estiver vazio, selecione o primeiro item da coleção. Caso contrário, retorne um valor nulo. A função de transformação vai remover os valores nulos.
data/WorkManagerBluromaticRepository.kt
import kotlinx.coroutines.flow.mapNotNull
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow().mapNotNull {
if (it.isNotEmpty()) it.first() else null
}
...
- Como a função de transformação
.mapNotNull()
garante a existência de um valor, é possível remover o?
do tipo do fluxo com segurança, já que ele não precisa mais ser um tipo anulável.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo> =
...
- Também é necessário remover o
?
da interfaceBluromaticRepository
.
data/BluromaticRepository.kt
...
interface BluromaticRepository {
// val outputWorkInfo: Flow<WorkInfo?>
val outputWorkInfo: Flow<WorkInfo>
...
As informações WorkInfo
são emitidas como um Flow
no repositório. Em seguida, o ViewModel
as consome.
Atualizar o BlurUiState
O ViewModel
usa a WorkInfo
emitida pelo repositório no fluxo outputWorkInfo
para definir o valor da variável blurUiState
.
O código da interface usa o valor da variável blurUiState
para determinar quais elementos combináveis são mostrados.
Conclua as etapas abaixo para executar a atualização da blurUiState
:
- Preencha a variável
blurUiState
com o fluxooutputWorkInfo
do repositório.
ui/BlurViewModel.kt
// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)
// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
- É necessário mapear os valores no fluxo para os estados da
BlurUiState
, dependendo do status do trabalho.
Quando o trabalho for concluído, defina a variável blurUiState
como BlurUiState.Complete(outputUri = "")
.
Quando o trabalho for cancelado, defina a variável blurUiState
como BlurUiState.Default
.
Caso contrário, defina o blurUiState
como BlurUiState.Loading
.
ui/BlurViewModel.kt
import androidx.work.WorkInfo
import kotlinx.coroutines.flow.map
// ...
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
.map { info ->
when {
info.state.isFinished -> {
BlurUiState.Complete(outputUri = "")
}
info.state == WorkInfo.State.CANCELLED -> {
BlurUiState.Default
}
else -> BlurUiState.Loading
}
}
// ...
- Como você tem interesse em um StateFlow, converta o fluxo encadeando uma chamada para a função
.stateIn()
.
A chamada para a função .stateIn()
requer três argumentos:
- Para o primeiro parâmetro, transmita
viewModelScope
, que é o escopo da corrotina vinculado ao ViewModel. - Para o segundo parâmetro, transmita
SharingStarted.WhileSubscribed(5_000)
. Esse parâmetro controla o início e a interrupção do compartilhamento. - Para o terceiro parâmetro, transmita
BlurUiState.Default
, que é o valor inicial do fluxo de estado.
ui/BlurViewModel.kt
import kotlinx.coroutines.flow.stateIn
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.SharingStarted
// ...
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
.map { info ->
when {
info.state.isFinished -> {
BlurUiState.Complete(outputUri = "")
}
info.state == WorkInfo.State.CANCELLED -> {
BlurUiState.Default
}
else -> BlurUiState.Loading
}
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = BlurUiState.Default
)
// ...
O ViewModel
expõe as informações do estado da interface como um StateFlow
usando a variável blurUiState
. O fluxo converte de um Flow
frio para um StateFlow
quente chamando a função stateIn()
(link em inglês).
Atualizar a interface
No arquivo ui/BluromaticScreen.kt
, você extrai o estado da interface da variável blurUiState
do ViewModel
e o atualiza.
Um bloco when
controla a interface do app. Esse bloco when
tem uma ramificação para cada um dos três estados do BlurUiState
.
A interface é atualizada no elemento combinável BlurActions
dentro do elemento Row
. Siga estas etapas:
- Remova o código
Button(onStartClick)
, no elemento combinávelRow
, e substitua-o por um blocowhen
comblurUiState
como argumento.
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
// REMOVE
// Button(
// onClick = onStartClick,
// modifier = Modifier.fillMaxWidth()
// ) {
// Text(stringResource(R.string.start))
// }
// ADD
when (blurUiState) {
}
}
...
Quando o app é aberto, ele fica no estado padrão. Esse estado no código é representado como BlurUiState.Default
.
- No bloco
when
, crie uma ramificação para esse estado, conforme mostrado no exemplo de código abaixo:
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {}
}
}
...
Para o estado padrão, o app mostra o botão Start.
- Para o parâmetro
onClick
no estadoBlurUiState.Default
, transmita a variávelonStartClick
, que está sendo transmitida ao elemento combinável. - Para o parâmetro
stringResourceId
, transmita o ID de recurso de string deR.string.start
.
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(
onClick = onStartClick,
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(R.string.start))
}
}
}
...
Quando o app está desfocando ativamente uma imagem, esse é o estado BlurUiState.Loading
. Para esse estado, o app mostra o botão Cancel Work (cancelar) e um indicador de progresso circular.
- Para o parâmetro
onClick
do botão no estadoBlurUiState.Loading
, transmita a variávelonCancelClick
, que está sendo transmitida ao elemento combinável. - Para o parâmetro
stringResourceId
do botão, transmita o ID de recurso de string deR.string.cancel_work
.
ui/BluromaticScreen.kt
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilledTonalButton
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
is BlurUiState.Loading -> {
FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
}
}
}
...
O último estado a ser configurado é o BlurUiState.Complete
, que ocorre depois que uma imagem é desfocada e salva. No momento, o app mostra apenas o botão Start.
- Para o parâmetro
onClick
no estadoBlurUiState.Complete
, transmita a variávelonStartClick
. - Para o parâmetro
stringResourceId
, transmita o ID de recurso de string deR.string.start
.
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
is BlurUiState.Loading -> {
FilledTonalButton(onCancelClick) { Text(stringResource(R.string.cancel_work)) }
CircularProgressIndicator(modifier = Modifier.padding(dimensionResource(R.dimen.padding_small)))
}
is BlurUiState.Complete -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
}
}
}
...
Executar o app
- Execute o app e clique em Start.
- Consulte a janela Background Task Inspector para conferir como os vários estados correspondem à interface mostrada.
SystemJobService
é o componente responsável por gerenciar as execuções do worker.
Enquanto os workers estão em execução, a interface mostra o botão Cancel Work e um indicador de progresso circular.
Depois que os workers terminam, a interface é atualizada para mostrar o botão Start conforme esperado.
5. Mostrar a saída final
Nesta seção, você vai configurar o app para ter um botão See File (mostrar arquivo) sempre que uma imagem desfocada estiver pronta para exibição.
Criar o botão See File.
O botão See File é mostrado apenas quando o BlurUiState
está no estado Complete
.
- Abra o arquivo
ui/BluromaticScreen.kt
e navegue até o elemento combinávelBlurActions
. - Para adicionar um espaço entre o botão Start e o botão See File, adicione um elemento combinável
Spacer
ao blocoBlurUiState.Complete
. - Adicione um novo elemento combinável
FilledTonalButton
. - Para o parâmetro
onClick
, transmitaonSeeFileClick(blurUiState.outputUri)
. - Adicione um elemento combinável
Text
para o parâmetro de conteúdo doButton
. - Para o parâmetro
text
doText
, use o ID do recurso de stringR.string.see_file
.
ui/BluromaticScreen.kt
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.width
// ...
is BlurUiState.Complete -> {
Button(onStartClick) { Text(stringResource(R.string.start)) }
// Add a spacer and the new button with a "See File" label
Spacer(modifier = Modifier.width(dimensionResource(R.dimen.padding_small)))
FilledTonalButton({ onSeeFileClick(blurUiState.outputUri) })
{ Text(stringResource(R.string.see_file)) }
}
// ...
Atualizar o blurUiState
O estado BlurUiState
é definido no ViewModel e depende do estado da solicitação de trabalho e, possivelmente, da variável bluromaticRepository.outputWorkInfo
.
- No arquivo
ui/BlurViewModel.kt
, na transformaçãomap()
, crie uma nova variáveloutputImageUri
. - Preencha o URI dessa nova imagem salva da variável no objeto de dados
outputData
.
É possível extrair essa string com a chave KEY_IMAGE_URI
.
ui/BlurViewModel.kt
import com.example.bluromatic.KEY_IMAGE_URI
// ...
.map { info ->
val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
when {
// ...
- Se o worker termina e a variável está preenchida, isso indica que há uma imagem desfocada para ser mostrada.
Para verificar se essa variável é preenchida, chame outputImageUri.isNullOrEmpty()
.
- Atualize a ramificação
isFinished
para verificar também se a variável está preenchida e, em seguida, transmita a variáveloutputImageUri
ao objeto de dadosBlurUiState.Complete
.
ui/BlurViewModel.kt
// ...
.map { info ->
val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
when {
info.state.isFinished && !outputImageUri.isNullOrEmpty() -> {
BlurUiState.Complete(outputUri = outputImageUri)
}
info.state == WorkInfo.State.CANCELLED -> {
// ...
Criar o código para o evento de clique do botão "See File"
Quando um usuário clica no botão See File, o gerenciador onClick
chama a função atribuída. Essa função é transmitida como um argumento na chamada para o elemento combinável BlurActions()
.
O objetivo dessa função é mostrar a imagem salva no URI. Ela chama a função auxiliar showBlurredImage()
e transmite o URI. A função auxiliar cria uma intent que é usada para iniciar uma nova atividade para mostrar a imagem salva.
- Abra o arquivo
ui/BluromaticScreen.kt
. - Na função
BluromaticScreenContent()
, na chamada para a função combinávelBlurActions()
, comece criando uma função lambda para o parâmetroonSeeFileClick
que usa um único parâmetro, chamadocurrentUri
. Essa abordagem armazena o URI da imagem salva.
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
onSeeFileClick = { currentUri ->
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
- No corpo da função lambda, chame a função auxiliar
showBlurredImage()
. - Para o primeiro parâmetro, transmita a variável
context
. - Para o segundo parâmetro, transmita a variável
currentUri
.
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
// New lambda code runs when See File button is clicked
onSeeFileClick = { currentUri ->
showBlurredImage(context, currentUri)
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
Executar seu app
Execute o app. Agora você vai notar o novo botão clicável See File, que leva ao arquivo salvo:
6. Cancelar um trabalho
Anteriormente, você adicionou o botão Cancel Work e agora pode adicionar o código para que ele faça algo. Com o WorkManager, é possível cancelar trabalhos usando o ID, a tag e o nome da cadeia exclusiva.
Nesse caso, é preciso cancelar o trabalho com o nome da cadeia única, porque você quer cancelar todo o trabalho na cadeia, não apenas uma etapa específica.
Cancelar um trabalho por nome
- Abra o arquivo
data/WorkManagerBluromaticRepository.kt
. - Na função
cancelWork()
, chame aworkManager.cancelUniqueWork()
. - Transmita o nome da cadeia única
IMAGE_MANIPULATION_WORK_NAME
para que a chamada cancele apenas o trabalho programado com esse nome.
data/WorkManagerBluromaticRepository.kt
override fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
Seguindo o princípio de design de separação de conceitos (link em inglês), as funções combináveis não podem interagir diretamente com o repositório. As funções combináveis interagem com o ViewModel, que interage com o repositório.
Essa abordagem é um bom princípio de design a ser seguido, porque mudanças no seu repositório não exigem que você mude as funções combináveis, porque elas não interagem diretamente.
- Abra o arquivo
ui/BlurViewModel.kt
. - Crie uma nova função com o nome
cancelWork()
para cancelar o trabalho. - Na função, no objeto
bluromaticRepository
, chame o métodocancelWork()
.
ui/BlurViewModel.kt
/**
* Call method from repository to cancel any ongoing WorkRequest
* */
fun cancelWork() {
bluromaticRepository.cancelWork()
}
Configurar o evento de clique para o botão "Cancel Work"
- Abra o arquivo
ui/BluromaticScreen.kt
. - Navegue até a função combinável
BluromaticScreen()
.
ui/BluromaticScreen.kt
fun BluromaticScreen(blurViewModel: BlurViewModel = viewModel(factory = BlurViewModel.Factory)) {
val uiState by blurViewModel.blurUiState.collectAsStateWithLifecycle()
val layoutDirection = LocalLayoutDirection.current
Surface(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding()
.padding(
start = WindowInsets.safeDrawing
.asPaddingValues()
.calculateStartPadding(layoutDirection),
end = WindowInsets.safeDrawing
.asPaddingValues()
.calculateEndPadding(layoutDirection)
)
) {
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = {},
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
}
}
Na chamada para o elemento combinável BluromaticScreenContent
, você quer que o método cancelWork()
do ViewModel seja executado quando um usuário clicar no botão.
- Atribua o valor
blurViewModel::cancelWork
ao parâmetrocancelWork
.
ui/BluromaticScreen.kt
// ...
BluromaticScreenContent(
blurUiState = uiState,
blurAmountOptions = blurViewModel.blurAmount,
applyBlur = blurViewModel::applyBlur,
cancelWork = blurViewModel::cancelWork,
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(dimensionResource(R.dimen.padding_medium))
)
// ...
Executar o app e cancelar um trabalho
Execute o app. A compilação vai acontecer sem problemas. Comece a desfocar uma imagem e clique em Cancel Work. A cadeia inteira será cancelada.
Depois que o trabalho é cancelado, apenas o botão Start aparece, porque o WorkInfo.State
fica no estado CANCELLED
. Essa mudança faz com que a variável blurUiState
seja definida como BlurUiState.Default
, o que redefine a interface de volta ao estado inicial e mostra apenas o botão Start.
A guia Background Task Inspector mostra o status Cancelled esperado.
7. Restrições de trabalho
Por último, mas não menos importante, o WorkManager
oferece suporte a Constraints
(restrições). Uma restrição é um requisito que precisa ser atendido antes que uma WorkRequest seja executada.
Alguns exemplos de restrições são requiresDeviceIdle()
e requiresStorageNotLow()
.
- Para a restrição
requiresDeviceIdle()
, se ela recebe um valor detrue
, o trabalho é executado apenas se o dispositivo está inativo. - Para a restrição
requiresStorageNotLow()
, se ela recebe um valor detrue
, o trabalho é executado apenas se o armazenamento não está baixo.
Para o Blur-O-Matic, adicione a restrição de que o nível de carga da bateria do dispositivo não pode estar baixo antes de executar a solicitação de trabalho blurWorker
. Com essa restrição, a solicitação de trabalho é adiada e só é executada quando a bateria do dispositivo não está baixa.
Criar restrição para bateria baixa
No arquivo data/WorkManagerBluromaticRepository.kt
, siga estas etapas:
- Navegue até o método
applyBlur()
. - Depois que o código declarar a variável
continuation
, crie uma nova variável chamadaconstraints
, que contém um objetoConstraints
para a restrição que está sendo criada. - Crie um builder para um objeto Constraints chamando a função
Constraints.Builder()
e o atribua à nova variável.
data/WorkManagerBluromaticRepository.kt
import androidx.work.Constraints
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
// ...
- Encadeie o método
setRequiresBatteryNotLow()
à chamada e transmita um valor detrue
para que oWorkRequest
seja executado apenas quando a bateria do dispositivo não estiver baixa.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
// ...
- Crie o objeto encadeando uma chamada ao método
.build()
.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build()
// ...
- Para adicionar o objeto de restrição à solicitação de trabalho
blurBuilder
, encadeie uma chamada ao método.setConstraints()
e transmita o objeto de restrição.
data/WorkManagerBluromaticRepository.kt
// ...
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))
blurBuilder.setConstraints(constraints) // Add this code
//...
Testar com emulador
- Em um emulador, mude o Charge level (nível da bateria) na janela Extended Controls para 15% ou menos para simular um cenário de bateria fraca, Charger connection para AC charger e Baterry status para Not charging.
- Execute o app e clique em Start para desfocar a imagem.
O nível de carga da bateria do emulador fica definido como baixo. Portanto, o WorkManager
não executa a solicitação de trabalho blurWorker
devido à restrição. Ela é colocada na fila, mas adiada até que a restrição seja atendida. É possível conferir esse adiamento na guia Background Task Inspector.
- Depois de confirmar que o trabalho não foi executado, aumente lentamente o nível de carga da bateria.
A restrição é atendida depois que o nível de carga da bateria atinge aproximadamente 25%. Após isso, o trabalho adiado é executado. Esse resultado aparece na guia Background Task Inspector.
8. Criar testes para implementações de workers
Como testar o WorkManager
Criar testes para workers e usar a API do WorkManager pode não ser intuitivo. O trabalho feito em um worker não tem acesso direto à interface; ele é estritamente uma lógica de negócios. Normalmente, você testa a lógica de negócios com testes de unidade locais. No entanto, como mostrado no codelab "Trabalho em segundo plano com o WorkManager", a API exige um contexto do Android. Por padrão, o contexto não está disponível em testes de unidade locais. Portanto, você precisa trabalhar em testes de workers com testes de interface, mesmo que não haja elementos de interface diretos a serem testados.
Configurar dependências
Você precisa adicionar três dependências do Gradle ao projeto. As duas primeiras ativam o JUnit e o Espresso para testes de interface. A terceira dependência fornece a API de testes de trabalho.
app/build.gradle.kts
dependencies {
// Espresso
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
// Junit
androidTestImplementation("androidx.test.ext:junit:1.1.5")
// Work testing
androidTestImplementation("androidx.work:work-testing:2.8.1")
}
Você precisa usar a versão estável mais recente do work-runtime-ktx
no app. Se você mudar a versão, clique em Sync Now para sincronizar seu projeto com os arquivos do Gradle atualizados.
Criar uma classe de testes
- Crie um diretório para seus testes de interface no diretório app > src.
- Crie uma nova classe do Kotlin com o nome
WorkerInstrumentationTest
no diretórioandroidTest/java
.
Criar um teste do CleanupWorker
Siga estas etapas para criar um teste e verificar a implementação do CleanupWorker
. Tente implementar essa verificação por conta própria com base nas instruções. A solução será fornecida no final das etapas.
- Em
WorkerInstrumentationTest.kt
, crie uma variávellateinit
para armazenar uma instância doContext
. - Crie um método
setUp()
com a anotação@Before
. - No método
setUp()
, inicialize a variável de contextolateinit
com um contexto de aplicativo deApplicationProvider
. - Crie uma função de testes com o nome
cleanupWorker_doWork_resultSuccess()
. - No teste
cleanupWorker_doWork_resultSuccess()
, crie uma instância doCleanupWorker
.
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
}
}
Ao criar o app Blur-O-Matic, você usa o OneTimeWorkRequestBuilder
para criar workers. O teste de workers exige diferentes builders de trabalho. A API do WorkManager fornece dois builders diferentes:
Com esses dois builders, é possível testar a lógica de negócios do worker. Para CoroutineWorkers
, por exemplo, CleanupWorker
, BlurWorker
e SaveImageToFileWorker
, use o TestListenableWorkerBuilder
para os testes, porque ele processa as complexidades da linha de execução da corrotina.
CoroutineWorker
s são executados de forma assíncrona, devido ao uso de corrotinas. Para executar o worker em paralelo, userunBlocking
. Forneça um corpo lambda vazio para começar, mas userunBlocking
para instruir o worker adoWork()
(realizar o trabalho) diretamente, em vez de enfileirar o worker.
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
runBlocking {
}
}
}
- No corpo lambda de
runBlocking
, chamedoWork()
na instância doCleanupWorker
que você criou na Etapa 5 e salve como um valor.
O CleanupWorker
exclui todos os arquivos PNG salvos na estrutura de arquivos do app Blur-O-Matic. Esse processo envolve a entrada/saída do arquivo, o que significa que exceções podem ser geradas ao tentar excluir arquivos. Por isso, a tentativa de excluir arquivos é encapsulada em um bloco try
.
CleanupWorker.kt
...
return@withContext try {
val outputDirectory = File(applicationContext.filesDir, OUTPUT_PATH)
if (outputDirectory.exists()) {
val entries = outputDirectory.listFiles()
if (entries != null) {
for (entry in entries) {
val name = entry.name
if (name.isNotEmpty() && name.endsWith(".png")) {
val deleted = entry.delete()
Log.i(TAG, "Deleted $name - $deleted")
}
}
}
}
Result.success()
} catch (exception: Exception) {
Log.e(
TAG,
applicationContext.resources.getString(R.string.error_cleaning_file),
exception
)
Result.failure()
}
Observe que, no final do bloco try
, Result.success()
é retornado. Se o código chegar a Result.success()
, não vai haver erros ao acessar o diretório do arquivo.
Agora, é hora de fazer uma declaração indicando que o worker foi executado corretamente.
- Afirme que o resultado do worker é
ListenableWorker.Result.success()
.
Confira o código da solução abaixo:
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
val worker = TestListenableWorkerBuilder<CleanupWorker>(context).build()
runBlocking {
val result = worker.doWork()
assertTrue(result is ListenableWorker.Result.Success)
}
}
}
Criar um teste do BlurWorker
Siga estas etapas para criar um teste e verificar a implementação do BlurWorker
. Tente implementar essa verificação por conta própria com base nas instruções. A solução será fornecida no final das etapas.
- Em
WorkerInstrumentationTest.kt
, crie uma nova função de teste com o nomeblurWorker_doWork_resultSuccessReturnsUri()
.
O BlurWorker
precisa de uma imagem para processar. Portanto, a criação de uma instância do BlurWorker
requer alguns dados de entrada que incluem essa imagem.
- Fora da função de teste, crie uma entrada de URI fictício. O URI fictício é um par que contém uma chave e um valor de URI. Use o código de exemplo abaixo para o par de chave-valor:
KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
- Crie um
BlurWorker
dentro da funçãoblurWorker_doWork_resultSuccessReturnsUri()
e transmita a entrada de URI fictício criada como dados de trabalho pelo métodosetInputData()
.
Assim como no teste do CleanupWorker
, é necessário chamar a implementação do worker dentro de runBlocking
.
- Crie um bloco
runBlocking
. - Chame
doWork()
dentro do blocorunBlocking
.
Ao contrário do CleanupWorker
, o BlurWorker
tem alguns dados de saída prontos para teste.
- Para acessar os dados de saída, extraia o URI do resultado de
doWork()
.
WorkerInstrumentationTest.kt
@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
val worker = TestListenableWorkerBuilder<BlurWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
}
}
- Faça uma declaração de que o worker teve sucesso. Por exemplo, confira este código do
BlurWorker
:
BlurWorker.kt
val resourceUri = inputData.getString(KEY_IMAGE_URI)
val blurLevel = inputData.getInt(BLUR_LEVEL, 1)
...
val picture = BitmapFactory.decodeStream(
resolver.openInputStream(Uri.parse(resourceUri))
)
val output = blurBitmap(picture, blurLevel)
// Write bitmap to a temp file
val outputUri = writeBitmapToFile(applicationContext, output)
val outputData = workDataOf(KEY_IMAGE_URI to outputUri.toString())
Result.success(outputData)
...
O BlurWorker
usa o URI e o nível de desfoque dos dados de entrada e cria um arquivo temporário. Se a operação tiver sucesso, ela vai retornar um par de chave-valor contendo o URI. Para verificar se o conteúdo da saída está correto, faça uma declaração de que os dados de saída contêm a chave KEY_IMAGE_URI
.
- Faça uma declaração de que os dados de saída contêm um URI que começa com a string
"file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-"
- Verifique seu teste em relação a este código da solução:
WorkerInstrumentationTest.kt
@Test
fun blurWorker_doWork_resultSuccessReturnsUri() {
val worker = TestListenableWorkerBuilder<BlurWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
assertTrue(result is ListenableWorker.Result.Success)
assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
assertTrue(
resultUri?.startsWith("file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-")
?: false
)
}
}
Criar um teste do SaveImageToFileWorker
Como o próprio nome indica, o SaveImageToFileWorker
grava um arquivo no disco. Não esqueça que, no WorkManagerBluromaticRepository
, você adiciona o SaveImageToFileWorker
ao WorkManager como uma continuação após o BlurWorker
. Ele tem os mesmos dados de entrada. Ele usa o URI dos dados de entrada, cria um bitmap e grava esse bitmap no disco como um arquivo. Se a operação for bem-sucedida, a saída resultante será um URL de imagem. O teste para o SaveImageToFileWorker
é muito semelhante ao do BlurWorker
. A única diferença são os dados de saída.
Tente criar um teste do SaveImageToFileWorker
por conta própria. Quando terminar, verifique a solução abaixo. Lembre-se da abordagem adotada para o teste do BlurWorker
:
- Crie o worker, transmitindo os dados de entrada.
- Crie um bloco
runBlocking
. - Chame
doWork()
no worker. - Verifique se o resultado foi bem-sucedido.
- Verifique a saída para encontrar a chave e o valor corretos.
Confira a solução:
@Test
fun saveImageToFileWorker_doWork_resultSuccessReturnsUrl() {
val worker = TestListenableWorkerBuilder<SaveImageToFileWorker>(context)
.setInputData(workDataOf(mockUriInput))
.build()
runBlocking {
val result = worker.doWork()
val resultUri = result.outputData.getString(KEY_IMAGE_URI)
assertTrue(result is ListenableWorker.Result.Success)
assertTrue(result.outputData.keyValueMap.containsKey(KEY_IMAGE_URI))
assertTrue(
resultUri?.startsWith("content://media/external/images/media/")
?: false
)
}
}
9. Depurar o WorkManager com o Inspetor de tarefas em segundo plano
Inspecionar workers
Os testes automatizados são uma ótima maneira de verificar a funcionalidade dos workers. No entanto, eles não são tão úteis quando você está tentando depurar um worker. Felizmente, o Android Studio tem uma ferramenta que permite visualizar, monitorar e depurar os workers em tempo real. O Inspetor de tarefas em segundo plano funciona para emuladores e dispositivos com o nível 26 da API ou mais recente.
Nesta seção, você vai aprender alguns dos recursos que o Inspetor de tarefas em segundo plano oferece para inspecionar os workers no Blur-O-Matic.
- Inicie o app Blur-O-Matic em um dispositivo ou emulador.
- Navegue até View > Tool Windows > App Inspection.
- Selecione a guia Background Task Inspector.
- Se necessário, selecione o dispositivo e o processo em execução no menu suspenso.
Nas imagens de exemplo, o processo é com.example.bluromatic
. Ele pode ser selecionado automaticamente para você. Se um processo errado for selecionado, você poderá selecionar outro.
- Clique no menu suspenso Workers. No momento, não há workers em execução, o que faz sentido porque não houve tentativa de desfocar uma imagem.
- No app, selecione More blurred e clique em Start. Um conteúdo aparece de imediato na lista suspensa Workers.
Agora, uma tela semelhante a esta vai aparecer no menu suspenso Workers.
A tabela Worker mostra o nome do worker, o serviço (SystemJobService
, neste caso), o status de cada um e um carimbo de data/hora. Na captura de tela da etapa anterior, observe que BlurWorker
e CleanupWorker
concluíram o trabalho.
Também é possível cancelar um trabalho usando o inspetor.
- Selecione um worker na fila e clique em Cancel Selected Worker na barra de ferramentas.
Inspecionar detalhes da tarefa
- Clique em um worker na tabela Workers.
Isso vai abrir a janela Task Details.
- Revise as informações que aparecem em Task Details.
Os detalhes mostram estas categorias:
- Description (descrição): lista o nome da classe de worker com o pacote totalmente qualificado, além da tag atribuída e o UUID desse worker.
- Execution (execução): mostra as restrições do worker, se houver, além da frequência de execução, o estado dele e qual classe criou e enfileirou esse worker. O BlurWorker tem uma restrição que impede a execução quando a bateria está fraca. Quando você inspeciona um worker que tem restrições, ele aparece nesta seção.
- WorkContinuation (continuação de trabalho): mostra a localização desse worker na cadeia de trabalho. Para verificar os detalhes de outro worker na cadeia, clique no UUID dele.
- Results (resultados): mostra o horário de início, a contagem de tentativas e os dados de saída do worker selecionado.
Visualização em gráfico
Não esqueça que os workers no Blur-O-Matic estão encadeados. O Inspetor de tarefas em segundo plano oferece uma visualização em gráfico que representa as dependências do worker visualmente.
No canto da janela Background Task Inspector, há dois botões para escolher entre Show Graph View e Show List View.
- Clique em Show Graph View:
A visualização em gráfico indica com precisão a dependência do worker implementada no app Blur-O-Matic.
- Clique em Show List View para sair da visualização em gráfico.
Mais recursos
O app Blur-O-Matic implementa apenas workers para concluir tarefas em segundo plano. Você pode ler mais sobre as ferramentas disponíveis para inspecionar outros tipos de trabalho em segundo plano na documentação do Inspetor de tarefas em segundo plano.
10. Acessar o código da solução
Para fazer o download do código do codelab concluído, use estes comandos:
$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-workmanager.git $ cd basic-android-kotlin-compose-training-workmanager $ git checkout main
Se preferir, você pode fazer o download do repositório como um arquivo ZIP, descompactar e abrir no Android Studio.
Confira o código da solução deste codelab no GitHub (link em inglês).
11. Parabéns
Parabéns! Você aprendeu sobre mais funcionalidades do WorkManger, criou testes automatizados para workers do Blur-O-Matic e usou o Inspetor de tarefas em segundo plano para análise. Neste codelab, você aprendeu como:
- Nomear cadeias
WorkRequest
exclusivas. - Incluir tags em
WorkRequest
s. - Atualizar a interface com base no
WorkInfo
. - Cancelar uma
WorkRequest
. - Adicionar restrições a uma
WorkRequest
. - Usar a API de teste do WorkManager.
- Abordar a implementação de workers de testes.
- Testar
CoroutineWorker
s. - Inspecionar workers manualmente e verificar a funcionalidade deles.