Saltar al contenido
K
Android: la plataformaPaso a paso25 min de lectura · 8 min de máquina Básico

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…UsaPorque
Puede esperar y tiene que ocurrir aunque la app se cierre («avísame antes del partido», «sube esto cuando haya red»)WorkManagerEl 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 serviceExige 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á abiertaUna corrutina normalSi 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:

PiezaQué es
CoroutineWorkerLa clase que representa una tarea. Sobrescribes doWork(), que es suspend
Result (del worker)Lo que devuelve doWork(): Result.success(), Result.failure() o Result.retry()
OneTimeWorkRequestLa petición: «ejecuta esta tarea una vez». Se construye con OneTimeWorkRequestBuilder<MiWorker>()
setInitialDelay(…)Cuánto esperar antes de ejecutarla
DataUn 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:

ejemplo mínimo: un worker con datos, retraso y nombre único (con comentarios)
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.
  • inputData es el paquete de datos (Data) con el que se encoló la tarea; dentro de doWork() lo lees con getString(clave). Y WorkerParameters no lo usas tú: solo lo pasas a CoroutineWorker, que lo guarda por ti.
  • Lo que va entre < > en OneTimeWorkRequestBuilder<SaludoWorker>() es la clase del worker que se ejecutará.
  • TimeUnit es una clase de Java con las unidades de tiempo: SECONDS, MINUTES, HOURS… setInitialDelay pide 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, como SaludoWorker.saludar(…). Lo viste en object, companion y vararg.

Reto · Ejercicio: ¿WorkManager, servicio o corrutina?

En el Kotlin Playground, fuera de tu proyecto:

  1. Modela los tres mecanismos como un tipo que solo pueda valer WorkManager, servicio en primer plano o corrutina.
  2. 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.
  3. 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_MANAGER
Ver solución
solución del reto
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.

Gradle Scripts
libs.versions.toml (Version Catalog "libs")se modifica: [versions] y [libraries]
build.gradle.kts (Module :app)se modifica: dependencies
Los dos ficheros de la instalación: el catálogo y el build.gradle.kts del módulo que usa la librería.

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]:

[versions]: la línea nueva (con comentarios)
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:

[libraries]: la línea nueva (con comentarios)
# 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:

build.gradle.kts (Module :app): línea nueva (con comentarios)
    // 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.toml tiene workRuntimeKtx = "2.12.0" en [versions] y androidx-work-runtime-ktx en [libraries], y build.gradle.kts (Module :app) tiene implementation(libs.androidx.work.runtime.ktx). Sync Now terminó sin errores rojos.
  • En la Terminal de Android Studio (View → Tool Windows → Terminal), ./gradlew :app:assembleDebug termina en BUILD SUCCESSFUL.

Comprueba lo que entendiste

4 preguntas · necesitas 3 para completar la lección

  1. 1.Quieres avisar al usuario una hora antes de un partido, aunque la app esté cerrada. ¿Qué eliges?

  2. 2.¿Qué obliga a hacer un foreground service que WorkManager no obliga?

  3. 3.Dentro de un CoroutineWorker, ¿qué es Result.success()?

  4. 4.En OneTimeWorkRequestBuilder<SaludoWorker>(), ¿qué va entre < >?

Faltan 4 de 4