Trabajo en segundo plano
Al terminar: Entender qué trabajo puede hacerse en segundo plano en Android e instalar WorkManager.
Un recordatorio como «el partido empieza en una hora» tiene un problema: cuando llegue la hora, tu app probablemente no estará abierta. El usuario habrá vuelto a la pantalla de inicio, o el sistema habrá cerrado el proceso para liberar memoria. Si el aviso dependiera de que tu código siga vivo, no llegaría.
Esta lección es la mitad de la teoría: aprenderás qué herramientas ofrece Android para trabajo fuera de la pantalla, cuál elegir y por qué, y dejarás instalada WorkManager en Marcador. En Un recordatorio con WorkManager la usarás para programar el aviso y comprobar que sobrevive a matar la app.
Trabajo que sobrevive a tu app
Android no te deja mantener un proceso vivo «por si acaso»: gasta batería y memoria. Para el trabajo que ocurre fuera de la pantalla ofrece dos herramientas, pensadas para casos distintos.
- WorkManager: tú describes una tarea y el sistema la ejecuta cuando toque, aunque la app esté cerrada.
- Foreground service (servicio en primer plano). Un servicio es una pieza de la app que trabaja sin pantalla; en primer plano significa que tu app sigue haciendo algo que el usuario nota mientras ocurre, y por eso muestra una notificación fija.
Con eso se decide en una pregunta: ¿el usuario nota este trabajo mientras ocurre?
| Si… | Usa | Porque |
|---|---|---|
| Puede esperar y tiene que ocurrir aunque la app se cierre («avísame antes del partido», «sube esto cuando haya red») | WorkManager | El sistema guarda la tarea y la ejecuta cuando toca |
| El usuario lo nota mientras ocurre (música, navegación, una descarga larga que ve avanzar) | Foreground service | Exige una notificación permanente y declarar en el Manifest un permiso y para qué sirve (música, ubicación…) |
| Solo importa mientras la app está abierta | Una corrutina normal | Si el proceso muere, el trabajo puede morir con él |
Un recordatorio puede esperar y no se ve avanzar: WorkManager. Marcador no necesita ningún foreground service.
Qué tiene WorkManager por dentro
WorkManager guarda cada tarea pendiente en una base de datos propia. Una base de datos es un fichero ordenado donde una app guarda datos que tienen que sobrevivir a que se cierre; harás una con Room en Room: entidades y DAO. Por eso la tarea sobrevive al cierre de la app y, según la documentación de Google, también a los reinicios del dispositivo. En la lección siguiente provocarás la muerte del proceso; el reinicio del móvil no.
Las piezas que vas a usar:
| Pieza | Qué es |
|---|---|
CoroutineWorker | La clase que representa una tarea. Sobrescribes doWork(), que es suspend |
Result (del worker) | Lo que devuelve doWork(): Result.success(), Result.failure() o Result.retry() |
OneTimeWorkRequest | La petición: «ejecuta esta tarea una vez». Se construye con OneTimeWorkRequestBuilder<MiWorker>() |
setInitialDelay(…) | Cuánto esperar antes de ejecutarla |
Data | Un paquete pequeño de datos (clave y valor) que viaja con la petición |
enqueueUniqueWork(nombre, política, petición) | Encola la tarea con un nombre. Si ya hay una con ese nombre, la política (REPLACE, por ejemplo) decide qué pasa |
Un worker mínimo, con datos, retraso y nombre único. Es una tarea inventada (un saludo), no la de Marcador:
class SaludoWorker(contexto: Context, parametros: WorkerParameters) : // WorkManager lo crea y le pasa estos dos
CoroutineWorker(contexto, parametros) { // hereda: así doWork puede ser suspend
override suspend fun doWork(): Result { // lo que hace la tarea, en un hilo de fondo
val nombre = inputData.getString(CLAVE_NOMBRE) ?: return Result.failure() // sin dato, termina con fallo
Log.d("Trabajo", "hola, $nombre")
return Result.success() // Result de WorkManager: terminó bien
}
companion object { // se usa sin crear un SaludoWorker
const val CLAVE_NOMBRE = "nombre" // la clave del dato, escrita una sola vez
fun saludar(contexto: Context, nombre: String) {
val peticion = OneTimeWorkRequestBuilder<SaludoWorker>() // «ejecuta SaludoWorker una vez»
.setInitialDelay(15, TimeUnit.SECONDS) // espera 15 segundos: número y unidad, por separado
.setInputData(Data.Builder().putString(CLAVE_NOMBRE, nombre).build()) // el paquete de datos
.build() // construye la petición
WorkManager.getInstance(contexto)
.enqueueUniqueWork("saludo", ExistingWorkPolicy.KEEP, peticion) // KEEP: si ya hay uno pendiente, ignora este
}
}
}Cinco cosas de ese código:
- El Builder (en español, «montador», como en Notificaciones; no es el constructor de una clase de Kotlin) es un objeto al que le vas pidiendo cosas, una por línea, y al final
build()lo da por terminado y te devuelve el resultado. Aquí monta la petición y el paquete de datos. inputDataes el paquete de datos (Data) con el que se encoló la tarea; dentro dedoWork()lo lees congetString(clave). YWorkerParametersno lo usas tú: solo lo pasas aCoroutineWorker, que lo guarda por ti.- Lo que va entre
< >enOneTimeWorkRequestBuilder<SaludoWorker>()es la clase del worker que se ejecutará. TimeUnites una clase de Java con las unidades de tiempo:SECONDS,MINUTES,HOURS…setInitialDelaypide el número y la unidad por separado.companion object: un sitio dentro de la clase para constantes y funciones que se llaman sin crear un objeto, comoSaludoWorker.saludar(…). Lo viste en object, companion y vararg.
Reto · Ejercicio: ¿WorkManager, servicio o corrutina?
En el Kotlin Playground, fuera de tu proyecto:
- Modela los tres mecanismos como un tipo que solo pueda valer WorkManager, servicio en primer plano o corrutina.
- Escribe una función que reciba si el usuario nota el trabajo mientras ocurre y si el trabajo tiene que sobrevivir al cierre de la app, y devuelva el mecanismo con estas reglas, comprobadas en este orden: si el usuario lo nota, servicio en primer plano; si no, pero tiene que sobrevivir al cierre de la app, WorkManager; en otro caso, corrutina.
- Clasifica con ella estos cuatro casos e imprime el mecanismo de cada uno, una línea por caso: reproducir música con la pantalla apagada, subir una foto cuando haya red, refrescar la lista mientras la pantalla está abierta, avisar «el partido empieza en una hora». Decidir qué le pasas a la función en cada caso es la parte del reto.
Lo que debe salir:
SERVICIO_EN_PRIMER_PLANO
WORK_MANAGER
CORRUTINA
WORK_MANAGERVer solución
enum class Mecanismo { WORK_MANAGER, SERVICIO_EN_PRIMER_PLANO, CORRUTINA }
fun elegir(elUsuarioLoNota: Boolean, tieneQueSobrevivirAlCierre: Boolean): Mecanismo = when {
elUsuarioLoNota -> Mecanismo.SERVICIO_EN_PRIMER_PLANO
tieneQueSobrevivirAlCierre -> Mecanismo.WORK_MANAGER
else -> Mecanismo.CORRUTINA
}
fun main() {
// música con la pantalla apagada: el usuario la nota -> servicio en primer plano
println(elegir(elUsuarioLoNota = true, tieneQueSobrevivirAlCierre = true))
// subir una foto cuando haya red: no se ve avanzar, debe sobrevivir -> WorkManager
println(elegir(elUsuarioLoNota = false, tieneQueSobrevivirAlCierre = true))
// refrescar la lista con la pantalla abierta: solo importa abierta -> corrutina
println(elegir(elUsuarioLoNota = false, tieneQueSobrevivirAlCierre = false))
// el aviso del partido: diferible y garantizado -> WorkManager
println(elegir(elUsuarioLoNota = false, tieneQueSobrevivirAlCierre = true))
}El orden de las ramas importa: se prueba primero «lo nota el usuario». Un trabajo que el usuario nota y que además debe sobrevivir al cierre es un servicio en primer plano, no WorkManager.
Paso · Instala WorkManager
Objetivo: que :app tenga WorkManager, instalado a mano a través del catálogo.
1 · Qué instalas. androidx.work:work-runtime-ktx es WorkManager, la librería de Google para dejar trabajo en manos del sistema; va en :app, que programa el aviso.
2 · [versions]. En libs.versions.toml (Version Catalog "libs"), al final de [versions]:
workRuntimeKtx = "2.12.0" # su propio número, en camelCase; una estable (evita -alpha, -beta y -rc)3 · [libraries]. Al final de la sección:
# group:name son las coordenadas; version.ref apunta a la línea de arriba
androidx-work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntimeKtx" }4 · Sin plugin. No tocas [plugins] ni la raíz.
5 · La dependencia. En build.gradle.kts (Module :app), debajo de la de libs.androidx.lifecycle.runtime.ktx:
// implementation: la usa el código de la app y va en el paquete que se publica (no es solo de tests ni solo de `debug`, como la `debugImplementation` de LeakCanary en [Context y Application](/aprender/android-context)).
// El alias es el del catálogo con los guiones hechos puntos
implementation(libs.androidx.work.runtime.ktx)6 · Sync Now. Sin errores rojos. No tocas el manifiesto: la librería trae el suyo y Gradle lo mezcla. La prueba llega en la lección siguiente, con import androidx.work.CoroutineWorker. Si sale rojo, mira «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto (aquí: alias con guiones donde van puntos, libs.androidx.work-runtime.ktx, o un dígito de más en la versión).
Punto de control
- En
Gradle Scripts,libs.versions.tomltieneworkRuntimeKtx = "2.12.0"en[versions]yandroidx-work-runtime-ktxen[libraries], ybuild.gradle.kts (Module :app)tieneimplementation(libs.androidx.work.runtime.ktx). Sync Now terminó sin errores rojos. - En la Terminal de Android Studio (View → Tool Windows → Terminal),
./gradlew :app:assembleDebugtermina enBUILD SUCCESSFUL.
Comprueba lo que entendiste
4 preguntas · necesitas 3 para completar la lección
1.Quieres avisar al usuario una hora antes de un partido, aunque la app esté cerrada. ¿Qué eliges?
2.¿Qué obliga a hacer un foreground service que WorkManager no obliga?
3.Dentro de un
CoroutineWorker, ¿qué esResult.success()?4.En
OneTimeWorkRequestBuilder<SaludoWorker>(), ¿qué va entre< >?