1. Introducción
En el codelab Trabajo en segundo plano con WorkManager, aprendiste a ejecutar trabajos en segundo plano (no en el subproceso principal) con WorkManager. En este codelab, seguirás aprendiendo sobre la funcionalidad de WorkManager para garantizar trabajos únicos, además de etiquetarlos, cancelarlos y restringirlos. Cuando completes el codelab, habrás aprendido a escribir pruebas automatizadas para verificar que tus trabajadores funcionen de manera correcta y muestren los resultados esperados. También aprenderás a usar el Inspector de tareas en segundo plano que proporciona Android Studio, para inspeccionar a los trabajadores en cola.
Qué compilarás
En este codelab, aprenderás a garantizar trabajos únicos, etiquetar trabajos y cancelarlos, además de implementar restricciones en ellos. Luego, aprenderás a escribir pruebas de IU automatizadas para la app Blur-O-Matic que verifican la funcionalidad de los tres trabajadores creados en el codelab Trabajo en segundo plano con WorkManager:
BlurWorker
CleanupWorker
SaveImageToFileWorker
Qué aprenderás
- Cómo garantizar un trabajo único
- Cómo cancelar un trabajo
- Cómo definir restricciones de trabajo
- Cómo escribir pruebas automatizadas para verificar la funcionalidad de los trabajadores
- Conceptos básicos para inspeccionar a los trabajadores en cola con el Inspector de tareas en segundo plano
Requisitos
- La versión estable más reciente de Android Studio
- Haber completado el codelab Trabajo en segundo plano con WorkManager
- Un dispositivo o emulador de Android
2. Cómo prepararte
Descarga el código
Haz clic en el siguiente vínculo para descargar todo el código de este codelab:
Si lo prefieres, también puedes clonar el codelab desde 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
Abre el proyecto en Android Studio.
3. Garantiza el trabajo único
Ahora que sabes cómo encadenar trabajadores, es momento de abordar otra poderosa función de WorkManager: las secuencias de trabajo únicas.
A veces, querrás que solo una cadena de trabajo se ejecute a la vez. Por ejemplo, tal vez tengas una cadena de trabajo que sincroniza tus datos locales con el servidor. Sería bueno permitir que la primera sincronización de datos termine antes de comenzar una nueva. Para hacerlo, deberás usar beginUniqueWork()
en lugar de beginWith()
y proporcionarle un nombre de String
único. Esto nombrará la cadena completa de solicitudes de trabajo para que puedas hacer consultas y búsquedas en todas ellas.
También debes pasar un objeto ExistingWorkPolicy
. Este objeto le indica al SO Android lo que sucede si el trabajo ya existe. Los valores de ExistingWorkPolicy
posibles son REPLACE
, KEEP
, APPEND
o APPEND_OR_REPLACE
.
En esta app, debes usar REPLACE
porque, si un usuario decide desenfocar otra imagen antes de que termine la actual, debes detener la actual y comenzar a desenfocar la imagen nueva.
También debes asegurarte de que, si un usuario hace clic en Start cuando una solicitud de trabajo ya está en cola, la app reemplazará la solicitud de trabajo anterior por la solicitud nueva. No tiene sentido seguir trabajando en la solicitud anterior porque la app la reemplaza por la nueva.
En el archivo data/WorkManagerBluromaticRepository.kt
, dentro del método applyBlur()
, completa los siguientes pasos:
- Quita la llamada a la función
beginWith()
y agrega una llamada a la funciónbeginUniqueWork()
. - Para el primer parámetro de la función
beginUniqueWork()
, pasa la constanteIMAGE_MANIPULATION_WORK_NAME
. - Para el segundo parámetro, el parámetro
existingWorkPolicy
, pasaExistingWorkPolicy.REPLACE
. - Para el tercer parámetro, crea una nueva
OneTimeWorkRequest
paraCleanupWorker
.
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)
)
...
Blur-O-Matic ahora desenfoca solo una imagen a la vez.
4. Etiqueta y actualiza la IU en función del estado del trabajo
El siguiente cambio que realices será sobre lo que mostrará la app cuando se ejecute el trabajo. La información que se muestra sobre los trabajos en cola determina cómo debe cambiar la IU.
En esta tabla, se muestran tres métodos diferentes a los que puedes llamar para obtener información sobre el trabajo:
Tipo | Método de WorkManager | Descripción |
Obtener trabajo con un ID | Esta función muestra un solo elemento LiveData<WorkInfo> para una WorkRequest específica por su ID. | |
Obtener trabajo con un nombre de cadena único | Esta función muestra LiveData<List<WorkInfo>> para todo el trabajo en una cadena única de WorkRequests. | |
Obtener trabajo con una etiqueta | Esta función muestra LiveData<List<WorkInfo>> para una etiqueta. |
Un objeto WorkInfo
contiene detalles sobre el estado actual de una WorkRequest
, incluido lo siguiente:
- Si el trabajo fue
BLOCKED
,CANCELLED
,ENQUEUED
,FAILED
,RUNNING
oSUCCEEDED
. - Si se completó la
WorkRequest
y los datos de salida del trabajo.
Estos métodos muestran LiveData. LiveData es un contenedor de datos observables optimizado para ciclos de vida. Para convertirlo en un flujo de objetos WorkInfo
, se llama a .asFlow()
.
Como te interesa cuándo se guarda la imagen final, debes agregar una etiqueta a la WorkRequest de SaveImageToFileWorker
para obtener su WorkInfo desde el método getWorkInfosByTagLiveData()
.
Otra opción es usar el método getWorkInfosForUniqueWorkLiveData()
, que muestra información sobre las tres WorkRequests (CleanupWorker
, BlurWorker
y SaveImageToFileWorker
). La desventaja de este método es que necesitas código adicional para encontrar específicamente la información de SaveImageToFileWorker
necesaria.
Cómo etiquetar la solicitud de trabajo
El etiquetado del trabajo se realiza en el archivo data/WorkManagerBluromaticRepository.kt
, dentro de la función applyBlur()
.
- Cuando crees la solicitud de trabajo
SaveImageToFileWorker
, etiqueta el trabajo con una llamada al métodoaddTag()
y pasa la constanteTAG_OUTPUT
deString
.
data/WorkManagerBluromaticRepository.kt
import com.example.bluromatic.TAG_OUTPUT
...
val save = OneTimeWorkRequestBuilder<SaveImageToFileWorker>()
.addTag(TAG_OUTPUT) // <- Add this
.build()
En lugar de un ID de WorkManager, debes usar una etiqueta en tu trabajo, ya que si tu usuario desenfoca varias imágenes, todas las WorkRequest
para guardar imágenes tendrán la misma etiqueta, pero no el mismo ID.
Cómo obtener WorkInfo
Usa la información WorkInfo
de la solicitud de trabajo SaveImageToFileWorker
en la lógica para decidir qué elementos componibles se muestran en la IU en función de BlurUiState
.
ViewModel consume esta información desde la variable outputWorkInfo
del repositorio.
Ahora que etiquetaste la solicitud de trabajo SaveImageToFileWorker
, puedes completar los siguientes pasos para recuperar su información:
- En el archivo
data/WorkManagerBluromaticRepository.kt
, llama al métodoworkManager.getWorkInfosByTagLiveData()
para propagar la variableoutputWorkInfo
. - Pasa la constante
TAG_OUTPUT
para el parámetro del método.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT)
...
La llamada del método getWorkInfosByTagLiveData()
muestra LiveData. LiveData es un contenedor de datos observables optimizado para ciclos de vida. La función .asFlow()
lo convierte en un flujo.
- Encadena una llamada a la función
.asFlow()
para convertir el método en un flujo. Debes convertir el método para que la app pueda trabajar con un flujo de Kotlin en lugar de LiveData.
data/WorkManagerBluromaticRepository.kt
import androidx.lifecycle.asFlow
...
override val outputWorkInfo: Flow<WorkInfo?> =
workManager.getWorkInfosByTagLiveData(TAG_OUTPUT).asFlow()
...
- Encadena una llamada a la función de transformación
.mapNotNull()
para asegurarte de que el flujo contenga valores. - Para la regla de transformación, si el elemento no está vacío, selecciona el primer elemento de la colección. De lo contrario, se mostrará un valor nulo. La función de transformación los quitará si son 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
}
...
- Debido a que la función de transformación
.mapNotNull()
garantiza que existe un valor, puedes quitar de forma segura el?
del tipo de flujo, ya que no necesita ser un tipo anulable.
data/WorkManagerBluromaticRepository.kt
...
override val outputWorkInfo: Flow<WorkInfo> =
...
- También debes quitar
?
de la interfaz deBluromaticRepository
.
data/BluromaticRepository.kt
...
interface BluromaticRepository {
// val outputWorkInfo: Flow<WorkInfo?>
val outputWorkInfo: Flow<WorkInfo>
...
La información WorkInfo
se emite como un Flow
del repositorio. Luego, el ViewModel
lo consume.
Cómo actualizar el BlurUiState
El ViewModel
usa la WorkInfo
que emite el repositorio del flujo outputWorkInfo
para establecer el valor de la variable blurUiState
.
El código de la IU usa el valor de la variable blurUiState
para determinar qué elementos componibles se muestran.
Completa los siguientes pasos para realizar la actualización de blurUiState
:
- Propaga la variable
blurUiState
con el flujooutputWorkInfo
del repositorio.
ui/BlurViewModel.kt
// ...
// REMOVE
// val blurUiState: StateFlow<BlurUiState> = MutableStateFlow(BlurUiState.Default)
// ADD
val blurUiState: StateFlow<BlurUiState> = bluromaticRepository.outputWorkInfo
// ...
- Luego, debes asignar los valores en el flujo a los estados
BlurUiState
, según el estado del trabajo.
Cuando finalice el trabajo, establece la variable blurUiState
en BlurUiState.Complete(outputUri = "")
.
Cuando se cancele el trabajo, establece la variable blurUiState
en BlurUiState.Default
.
De lo contrario, establece la variable blurUiState
en 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 estás interesado en un StateFlow, convierte el flujo encadenando una llamada a la función
.stateIn()
.
La llamada a la función .stateIn()
requiere tres argumentos:
- Para el primer parámetro, pasa
viewModelScope
, que es el alcance de la corrutina vinculado al ViewModel. - Para el segundo parámetro, pasa
SharingStarted.WhileSubscribed(5_000)
. Este parámetro controla cuándo se inicia y se detiene el uso compartido. - Para el tercer parámetro, pasa
BlurUiState.Default
, que es el valor inicial del flujo 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
)
// ...
El ViewModel
expone la información del estado de la IU como un StateFlow
a través de la variable blurUiState
. El flujo convierte un Flow
frío en un StateFlow
caliente mediante una llamada a la función stateIn()
.
Cómo actualizar la IU
En el archivo ui/BluromaticScreen.kt
, debes obtener el estado de la IU a partir de la variable blurUiState
de ViewModel
y actualizar la IU.
Un bloque when
controla la IU de la app. Este bloque when
tiene una rama para cada uno de los tres estados de BlurUiState
.
La IU se actualiza en el elemento componible BlurActions
dentro de su elemento componible Row
. Completa los siguientes pasos:
- Quita el código
Button(onStartClick)
dentro elemento componibleRow
y reemplázalo por un bloquewhen
conblurUiState
como su 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) {
}
}
...
Cuando la app se abre, se encuentra en su estado predeterminado. En el código, este estado se representa como BlurUiState.Default
.
- Dentro del bloque
when
, crea una rama para este estado, como se muestra en el siguiente ejemplo de código:
ui/BluromaticScreen.kt
...
Row(
modifier = modifier,
horizontalArrangement = Arrangement.Center
) {
when (blurUiState) {
is BlurUiState.Default -> {}
}
}
...
En el estado predeterminado, la app muestra el botón Start.
- Para el parámetro
onClick
en el estadoBlurUiState.Default
, pasa la variableonStartClick
, que se pasa al elemento componible. - Para el parámetro
stringResourceId
, pasa el ID de recurso de strings 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))
}
}
}
...
Cuando la app está desenfocando una imagen, el estado es: BlurUiState.Loading
. Para este estado, la app muestra el botón Cancel Work y un indicador de progreso circular.
- Para el parámetro
onClick
del botón en el estadoBlurUiState.Loading
, pasa la variableonCancelClick
, que se pasa al elemento componible. - Para el parámetro
stringResourceId
del botón, pasa el ID de recurso de strings 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)))
}
}
}
...
El último estado que se debe configurar es el BlurUiState.Complete
, que ocurre después de que una imagen se desenfoca y se guarda. En este momento, la app solo muestra el botón Start.
- Para su parámetro
onClick
en el estadoBlurUiState.Complete
, pasa la variableonStartClick
. - Para su parámetro
stringResourceId
, pasa el ID de recurso de strings 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)) }
}
}
}
...
Ejecuta tu app
- Ejecuta la app y haz clic en Start.
- Consulta la ventana del Inspector de tareas en segundo plano para ver la correspondencia entre los distintos estados y la IU que se muestra.
SystemJobService
es el componente responsable de administrar ejecuciones de Worker.
Mientras se ejecutan los trabajadores, la IU muestra el botón Cancel Work y un indicador de progreso circular.
Una vez que los trabajadores finalizan, la IU se actualiza para mostrar el botón Start como estaba previsto.
5. Muestra el resultado final
En esta sección, configurarás la app para que muestre un botón con la etiqueta See File cuando haya una imagen desenfocada lista para mostrarse.
Cómo crear el botón See File
El botón See File solo se muestra cuando BlurUiState
es Complete
.
- Abre el archivo
ui/BluromaticScreen.kt
y navega hasta el elemento componibleBlurActions
. - Para agregar espacio entre el botón Start y el botón See File, agrega un elemento
Spacer
componible dentro del bloqueBlurUiState.Complete
. - Agrega un nuevo elemento componible
FilledTonalButton
. - Para el parámetro
onClick
, pasaonSeeFileClick(blurUiState.outputUri)
. - Agrega un elemento componible
Text
al parámetro de contenido deButton
. - Para el parámetro
text
deText
, usa el ID de recurso de stringsR.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)) }
}
// ...
Cómo actualizar BlurUIState
El estado BlurUiState
se establece en el ViewModel y depende del estado de la solicitud de trabajo y, posiblemente, de la variable bluromaticRepository.outputWorkInfo
.
- En el archivo
ui/BlurViewModel.kt
, dentro de la transformaciónmap()
, crea una nueva variableoutputImageUri
. - Propaga el URI de esta imagen guardada de la nueva variable desde el objeto de datos
outputData
.
Puedes recuperar esta string con la clave KEY_IMAGE_URI
.
ui/BlurViewModel.kt
import com.example.bluromatic.KEY_IMAGE_URI
// ...
.map { info ->
val outputImageUri = info.outputData.getString(KEY_IMAGE_URI)
when {
// ...
- Si el trabajador finaliza y la variable se propaga, significa que existe una imagen desenfocada para mostrar.
Puedes verificar si se propaga esta variable si llamas a outputImageUri.isNullOrEmpty()
.
- Actualiza la rama
isFinished
para verificar también que la variable se propague y, luego, pasa la variableoutputImageUri
al objeto de datosBlurUiState.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 -> {
// ...
Cómo crear el código de evento de clic para See File
Cuando un usuario hace clic en el botón See File, su controlador onClick
llama a la función asignada. Esta función pasa como un argumento en la llamada al elemento componible BlurActions()
.
El propósito de esta función es mostrar la imagen guardada desde su URI. Llama a la función auxiliar showBlurredImage()
y pasa el URI. La función auxiliar crea un intent y lo usa para iniciar una nueva actividad para mostrar la imagen guardada.
- Abre el archivo
ui/BluromaticScreen.kt
. - En la función
BluromaticScreenContent()
, en la llamada a la función de componibilidadBlurActions()
, comienza a crear una función lambda para el parámetroonSeeFileClick
que tome un solo parámetro llamadocurrentUri
. Este enfoque almacena el URI de la imagen guardada.
ui/BluromaticScreen.kt
// ...
BlurActions(
blurUiState = blurUiState,
onStartClick = { applyBlur(selectedValue) },
onSeeFileClick = { currentUri ->
},
onCancelClick = { cancelWork() },
modifier = Modifier.fillMaxWidth()
)
// ...
- Dentro del cuerpo de la función lambda, llama a la función auxiliar
showBlurredImage()
. - Para el primer parámetro, pasa la variable
context
. - Para el segundo parámetro, pasa la variable
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()
)
// ...
Cómo ejecutar tu app
Ejecuta la app. Ahora verás el nuevo botón See File en el que se puede hacer clic, que te llevará al archivo guardado:
6. Cómo cancelar el trabajo
Anteriormente, agregaste el botón Cancel Work, por lo que ahora puedes agregar el código para que realice una acción. Con WorkManager, puedes cancelar trabajos usando el ID, la etiqueta y el nombre de una cadena única.
En este caso, quieres cancelar el trabajo con su nombre de cadena único, ya que quieres cancelar todo el trabajo de la cadena, no solo un paso en particular.
Cómo cancelar el trabajo por nombre
- Abre el archivo
data/WorkManagerBluromaticRepository.kt
. - En la función
cancelWork()
, llama a la funciónworkManager.cancelUniqueWork()
. - Pasa el nombre de cadena único
IMAGE_MANIPULATION_WORK_NAME
para que la llamada solo cancele el trabajo programado con ese nombre.
data/WorkManagerBluromaticRepository.kt
override fun cancelWork() {
workManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME)
}
Según el principio de diseño de separación de problemas, las funciones de componibilidad no deben interactuar directamente con el repositorio. Las funciones de componibilidad interactúan con ViewModel, y este último interactúa con el repositorio.
Este enfoque es un buen principio de diseño a seguir porque los cambios en tu repositorio no requieren que cambies tus funciones de componibilidad, ya que no interactúan directamente.
- Abre el archivo
ui/BlurViewModel.kt
. - Crea una función nueva llamada
cancelWork()
para cancelar el trabajo. - Dentro de la función, en el objeto
bluromaticRepository
, llama al métodocancelWork()
.
ui/BlurViewModel.kt
/**
* Call method from repository to cancel any ongoing WorkRequest
* */
fun cancelWork() {
bluromaticRepository.cancelWork()
}
Cómo configurar un evento de clic de cancelación de trabajo
- Abre el archivo
ui/BluromaticScreen.kt
. - Navega a la función de componibilidad
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))
)
}
}
Dentro de la llamada al elemento componible BluromaticScreenContent
, quieres que se ejecute el método cancelWork()
de ViewModel cuando un usuario haga clic en el botón.
- Asigna el parámetro
cancelWork
al valorblurViewModel::cancelWork
.
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))
)
// ...
Cómo ejecutar tu app y cancelar el trabajo
Ejecuta la app y comprueba que se compile correctamente. Comienza a desenfocar una imagen y haz clic en Cancel Work. Se cancelará toda la cadena.
Después de cancelar el trabajo, solo se mostrará el botón Start porque WorkInfo.State
es CANCELLED
. Este cambio hace que la variable blurUiState
se establezca en BlurUiState.Default
. Como resultado, la IU se restablece a su estado inicial y solo muestra el botón Start.
El Inspector de tareas en segundo plano muestra el estado Cancelled previsto.
7. Restricciones de trabajos
Por último, WorkManager
admite Constraints
. Una restricción es un requisito que debes cumplir antes de ejecutar una WorkRequest.
Algunos ejemplos de restricciones son requiresDeviceIdle()
y requiresStorageNotLow()
.
- Para la restricción
requiresDeviceIdle()
, si se le pasa un valor detrue
, el trabajo se ejecuta solo cuando el dispositivo está inactivo. - Para la restricción
requiresStorageNotLow()
, si se le pasa un valor detrue
, el trabajo se ejecuta solo cuando no queda poco almacenamiento.
Para Blur-O-Matic, debes agregar la restricción de que el nivel de carga de la batería del dispositivo no debe ser bajo antes de ejecutar la solicitud de trabajo de blurWorker
. Esta restricción implica que tu solicitud de trabajo se aplaza y solo se ejecuta cuando el dispositivo no tenga batería baja.
Cómo crear una restricción de batería no baja
En el archivo data/WorkManagerBluromaticRepository.kt
, completa los siguientes pasos:
- Navega al método
applyBlur()
. - Después del código que declara la variable
continuation
, crea una variable nueva llamadaconstraints
, que contendrá un objetoConstraints
para la restricción que se creará. - Crea un compilador para un objeto Constraints. Para ello, llama a la función
Constraints.Builder()
y asígnala a la nueva variable.
data/WorkManagerBluromaticRepository.kt
import androidx.work.Constraints
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
// ...
- Encadena el método
setRequiresBatteryNotLow()
a la llamada y pásale un valor detrue
para queWorkRequest
solo se ejecute cuando la batería del dispositivo no esté baja.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
// ...
- Compila el objeto mediante el encadenamiento de una llamada al método
.build()
.
data/WorkManagerBluromaticRepository.kt
// ...
override fun applyBlur(blurLevel: Int) {
// ...
val constraints = Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build()
// ...
- Para agregar el objeto de restricción a la solicitud de trabajo
blurBuilder
, encadena una llamada al método.setConstraints()
y pasa el objeto de restricción.
data/WorkManagerBluromaticRepository.kt
// ...
blurBuilder.setInputData(createInputDataForWorkRequest(blurLevel, imageUri))
blurBuilder.setConstraints(constraints) // Add this code
//...
Cómo realizar una prueba con el emulador
- En un emulador, cambia la opción Charge level en la ventana Extended Controls para que sea del 15% o inferior, y simular una situación de batería baja; con Charger connection en AC charger y Battery status en Not charging.
- Ejecuta la app y haz clic en Start para desenfocar la imagen.
El nivel de carga de la batería del emulador es bajo, por lo que WorkManager
no ejecuta la solicitud de trabajo blurWorker
debido a la restricción. Está en cola, pero se aplaza hasta que se cumpla la restricción. Puedes ver este aplazamiento en la pestaña del Inspector de tareas en segundo plano.
- Después de confirmar que no se ejecutó, aumenta lentamente el nivel de carga de la batería.
La restricción se alcanza después de que el nivel de carga de la batería alcanza aproximadamente el 25%, y se ejecuta el trabajo aplazado. Este resultado aparecerá en la pestaña del Inspector de tareas en segundo plano.
8. Escribe pruebas para las implementaciones de Worker
Cómo probar WorkManager
La escritura de pruebas para Workers y la realización de pruebas con la API de WorkManager puede ser contradictorio. El trabajo que se realiza en un Worker no tiene acceso directo a la IU; es estrictamente una lógica empresarial. Por lo general, la lógica empresarial se prueba con pruebas de unidades locales. Sin embargo, debes recordar el codelab sobre el trabajo en segundo plano con WorkManager que WorkManger requiere un contexto de Android para ejecutarse. El contexto no está disponible de forma predeterminada en las pruebas de unidades locales. Por lo tanto, debes realizar las pruebas de Worker con pruebas de IU, aunque no haya elementos directos de la IU para probar.
Cómo configurar dependencias
Debes agregar tres dependencias de Gradle a tu proyecto. Las dos primeras habilitan JUnit y Espresso para pruebas de IU. La tercera dependencia proporciona la API de prueba de trabajo.
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")
}
Debes usar la versión estable más reciente de work-runtime-ktx
en tu app. Si cambias la versión, asegúrate de hacer clic en Sync Now para sincronizar tu proyecto con los archivos de Gradle actualizados.
Cómo crear una clase de prueba
- Crea un directorio para las pruebas de tu IU en el directorio app > src.
- Crea una nueva clase de Kotlin en el directorio
androidTest/java
llamadaWorkerInstrumentationTest
.
Cómo escribir una prueba de CleanupWorker
Sigue los pasos a continuación para escribir una prueba que verifique la implementación de CleanupWorker
. Intenta implementar esta verificación por tu cuenta siguiendo las instrucciones. La solución se proporciona al final de los pasos.
- En
WorkerInstrumentationTest.kt
, crea una variablelateinit
que contenga una instancia deContext
. - Crea un método
setUp()
anotado con@Before
. - En el método
setUp()
, inicializa la variable de contextolateinit
con un contexto de aplicación deApplicationProvider
. - Crea una función de prueba llamada
cleanupWorker_doWork_resultSuccess()
. - En la prueba
cleanupWorker_doWork_resultSuccess()
, crea una instancia deCleanupWorker
.
WorkerInstrumentationTest.kt
class WorkerInstrumentationTest {
private lateinit var context: Context
@Before
fun setUp() {
context = ApplicationProvider.getApplicationContext()
}
@Test
fun cleanupWorker_doWork_resultSuccess() {
}
}
Cuando escribes la app Blur-O-Matic, usas OneTimeWorkRequestBuilder
para crear trabajadores. Para probar Workers, se requieren diferentes compiladores de trabajos. La API de WorkManager proporciona dos compiladores diferentes:
Ambos compiladores te permiten probar la lógica empresarial de tu trabajador. Para CoroutineWorkers
, como CleanupWorker
, BlurWorker
y SaveImageToFileWorker
, usa TestListenableWorkerBuilder
para las pruebas, ya que controla las complejidades de subprocesos de la corrutina.
- Los
CoroutineWorker
se ejecutan de forma asíncrona, dado el uso de corrutinas. Para ejecutar el trabajador en paralelo, usarunBlocking
. Proporciona un cuerpo de lambda vacío para comenzar, pero usarunBlocking
para indicarle al trabajador quedoWork()
directamente, en lugar de ponerlo en cola.
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 {
}
}
}
- En el cuerpo de la lambda de
runBlocking
, llama adoWork()
en la instancia deCleanupWorker
que creaste en el paso 5 y guárdalo como valor.
Quizás recuerdes que CleanupWorker
borra todos los archivos PNG guardados en la estructura de archivos de la app de Blur-O-Matic. Este proceso abarca la entrada y salida de archivos, lo que significa que se pueden generar excepciones mientras se intenta borrar archivos. Por este motivo, el intento de borrar archivos se une en un bloque 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()
}
Ten en cuenta que, al final del bloque try
, se muestra Result.success()
. Si el código llega a Result.success()
, no se produce un error al acceder al directorio de archivos.
Ahora es el momento de crear una aserción que indique que el trabajador tuvo éxito.
- Confirma que el resultado del trabajador es
ListenableWorker.Result.success()
.
Observa el siguiente código de solución:
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)
}
}
}
Cómo escribir una prueba de BlurWorker
Sigue estos pasos para escribir una prueba y verificar la implementación de BlurWorker
. Intenta implementar esta verificación por tu cuenta siguiendo las instrucciones. La solución se proporciona al final de los pasos.
- En
WorkerInstrumentationTest.kt
, crea una nueva función de prueba llamadablurWorker_doWork_resultSuccessReturnsUri()
.
BlurWorker
necesita una imagen para procesarse. Por lo tanto, compilar una instancia de BlurWorker
requiere algunos datos de entrada que incluyan esa imagen.
- Fuera de la función de prueba, crea una entrada de URI de prueba. El URI de prueba es un par que contiene una clave y un valor de URI. Usa el siguiente código de ejemplo para el par clave-valor:
KEY_IMAGE_URI to "android.resource://com.example.bluromatic/drawable/android_cupcake"
- Compila un
BlurWorker
dentro de la funciónblurWorker_doWork_resultSuccessReturnsUri()
y asegúrate de pasar la entrada de URI de prueba que crees como datos de trabajo a través del métodosetInputData()
.
Al igual que con la prueba CleanupWorker
, debes llamar a la implementación del trabajador dentro de runBlocking
.
- Crea un bloque
runBlocking
. - Llama a
doWork()
dentro del bloquerunBlocking
.
A diferencia de CleanupWorker
, BlurWorker
tiene algunos datos de salida que están listos para que los puedas probar.
- Para acceder a los datos de salida, extrae el URI del 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)
}
}
- Crea una aserción de que el trabajador es correcto. Por ejemplo, observa el siguiente código de
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)
...
El BlurWorker
toma el URI y el nivel de desenfoque de los datos de entrada y crea un archivo temporal. Si la operación se realiza correctamente, se mostrará un par clave-valor que contiene el URI. Para verificar que el contenido de la salida sea correcto, crea una aserción de que los datos de salida contienen la clave KEY_IMAGE_URI
.
- Crea una aserción de que los datos de salida contienen un URI que comienza con la string
"file:///data/user/0/com.example.bluromatic/files/blur_filter_outputs/blur-filter-output-"
.
- Verifica tu prueba con el siguiente código de solución:
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
)
}
}
Cómo escribir una prueba de SaveImageToFileWorker
Como lo indica su nombre, SaveImageToFileWorker
escribe un archivo en el disco. Recuerda que, en WorkManagerBluromaticRepository
, agregas SaveImageToFileWorker
a WorkManager como una continuación de BlurWorker
. Por lo tanto, tiene los mismos datos de entrada. Toma el URI de los datos de entrada, crea un mapa de bits y, luego, lo escribe en el disco como un archivo. Si la operación se realiza correctamente, el resultado será una URL de imagen. La prueba para SaveImageToFileWorker
es muy similar a la de BlurWorker
. La única diferencia son los datos de salida.
Intenta escribir una prueba para SaveImageToFileWorker
por tu cuenta. Cuando termines, puedes consultar la solución que se muestra a continuación. Recuerda el enfoque que usaste para la prueba BlurWorker
:
- Compila el trabajador y pasa los datos de entrada.
- Crea un bloque
runBlocking
. - Llama a
doWork()
en el trabajador. - Comprueba que el resultado se haya realizado correctamente.
- Verifica el resultado de la clave y el valor correctos.
Esta es la solución:
@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. Depura WorkManager con el Inspector de tareas en segundo plano
Cómo inspeccionar trabajadores
Las pruebas automatizadas son una excelente manera de verificar la funcionalidad de tus trabajadores. Sin embargo, no son tan útiles cuando intentas depurar un trabajador. Afortunadamente, Android Studio cuenta con una herramienta que te permite visualizar, supervisar y depurar tus trabajadores en tiempo real. El Inspector de tareas en segundo plano funciona en emuladores y dispositivos con nivel de API 26 o posterior.
En esta sección, conocerás algunas de las funciones que proporciona el Inspector de tareas en segundo plano para inspeccionar a los trabajadores en Blur-O-Matic.
- Inicia la app de Blur-O-Matic en un dispositivo o emulador.
- Ve a View > Tool Windows > App Inspection.
- Selecciona la pestaña del Inspector de tareas en segundo plano.
- Si es necesario, selecciona el dispositivo y el proceso en ejecución en el menú desplegable.
En las imágenes de ejemplo, el proceso es com.example.bluromatic
. Puede seleccionar el proceso automáticamente. Si selecciona el proceso equivocado, puedes cambiarlo.
- Haz clic en el menú desplegable Workers. Actualmente, no hay trabajadores en ejecución, lo cual tiene sentido, ya que no se intentó desenfocar una imagen.
- En la app, selecciona More blurred y haz clic en Start. De inmediato, verás contenido en el menú desplegable Workers.
Verás algo como esto en el menú desplegable Workers.
En la tabla de trabajadores, se muestra el nombre del trabajador, el servicio (en este caso, SystemJobService
), el estado de cada uno y una marca de tiempo. En la captura de pantalla del paso anterior, observa que BlurWorker
y CleanupWorker
completaron correctamente su trabajo.
También puedes cancelar el trabajo con el inspector.
- Selecciona un trabajador en cola y haz clic en Cancel Selected Worker en la barra de herramientas.
Cómo inspeccionar los detalles de la tarea
- Haz clic en un trabajador en la tabla Workers.
Cuando lo hagas, se abrirá la ventana Task Details.
- Revisa la información que se muestra en Task Details.
En los detalles, se muestran las siguientes categorías:
- La sección Description enumera el nombre de la clase de trabajador con el paquete completamente calificado, así como la etiqueta asignada y el UUID de ese trabajador.
- La sección Execution muestra las restricciones del trabajador (si existen), la frecuencia de ejecución, su estado y la clase que creó y puso en cola este trabajador. Recuerda que el BlurWorker tiene una restricción que le impide ejecutarse cuando la batería está baja. Cuando inspeccionas un trabajador que tiene restricciones, aparece en esta sección.
- La sección WorkContinuation muestra dónde se encuentra este trabajador en la cadena de trabajo. Para verificar los detalles de otro trabajador de la cadena de trabajo, haz clic en su UUID.
- La sección Results muestra la hora de inicio, la cantidad de reintentos y los datos de salida del trabajador seleccionado.
Vista de gráfico
Recuerda que los trabajadores de Blur-O-Matic están encadenados. El Inspector de tareas en segundo plano ofrece una vista de gráfico que representa visualmente las dependencias de los trabajadores.
En la esquina de la ventana del Inspector de tareas en segundo plano, hay dos botones para activar o desactivar: Show Graph View y Show List View.
- Haz clic en Show Graph View :
La vista de gráfico indica con exactitud la dependencia de Worker implementada en la app de Blur-O-Matic.
- Haz clic en Show List View para salir de la vista de gráfico.
Funciones adicionales
La app de Blur-O-Matic solo implementa Workers para completar tareas en segundo plano. Sin embargo, puedes leer más sobre las herramientas disponibles para inspeccionar otros tipos de tareas en segundo plano en la documentación del Inspector de tareas en segundo plano.
10. Obtén el código de la solución
Para descargar el código del codelab terminado, puedes usar estos 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
También puedes descargar el repositorio como un archivo ZIP, descomprimirlo y abrirlo en Android Studio.
Si deseas ver el código de la solución para este codelab, míralo en GitHub.
11. Felicitaciones
¡Felicitaciones! Aprendiste sobre la funcionalidad adicional de WorkManger, escribiste pruebas automatizadas para los trabajadores de Blur-O-Matic y usaste el Inspector de tareas en segundo plano para examinarlas. En este codelab, aprendiste lo siguiente:
- Cómo asignar nombres a cadenas de
WorkRequest
únicas - Cómo etiquetar
WorkRequest
- Cómo actualizar la IU en función de
WorkInfo
- Cómo cancelar una
WorkRequest
- Cómo agregar restricciones a una
WorkRequest
- Cómo usar API de prueba de WorkManager
- Cómo abordar las implementaciones de trabajadores de prueba
- Cómo probar
CoroutineWorker
- Cómo inspeccionar manualmente a los trabajadores y verificar su funcionalidad