Saltar al contenido
K
El Pokémon del díaPaso a paso34 min Avanzado

Trabajo en segundo plano en Android

Al terminar: Programar el aviso diario con WorkManager y probarlo sin esperar veinticuatro horas.

La notificación ya se muestra, pero solo cuando tú la pides tocando la fila de Ajustes. Falta lo que hace que sea un recordatorio: que salga sola, cada día, sin que la app esté abierta.

Aquí aparece la primera idea que hay que aceptar sobre el segundo plano en Android moderno: no vas a decidir el momento exacto. El sistema agrupa tu trabajo con el de otras apps para no despertar el móvil más de lo necesario, y eso no es un defecto que se pueda sortear, es el trato. Vamos a ver dónde está el límite, por qué para este caso sobra, y cómo se comprueba que funciona sin esperar veinticuatro horas, que es más difícil de lo que parece y donde la lección tiene su sorpresa.

gradle
libs.versions.tomlse modifica: [versions] y [libraries]
androidApp
build.gradle.ktsse modifica: dependencies
src/main/kotlin/com/sebastiannarvaez/pokedex/android
daily/RecordatorioDiario.ktse crea
settings/SettingsScreen.ktse modifica: una llamada, y se quita hoy()
Un fichero nuevo, dos de Gradle y uno de la pantalla de Ajustes (el de Ajustes pierde una función y gana una llamada).

Lo nuevo, con un ejemplo ajeno al proyecto

Un trabajo de una sola vez se ejecuta y termina. Un trabajo periódico es otra cosa: le dices «repite esto cada cierto tiempo» y el sistema lo vuelve a lanzar solo, una vez por periodo, hasta que alguien lo cancele. Lo construyes con PeriodicWorkRequestBuilder (la «petición» periódica) y lo entregas con enqueueUniquePeriodicWork, que además le pone nombre para que no puedan existir dos iguales. Un ejemplo con una app de plantas que avisa de regar cada tres días. No hace falta ejecutarlo; está para leerlo antes de ver el de la Pokédex:

ejemplo mínimo: un aviso de riego cada tres días (con comentarios)
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.ExistingPeriodicWorkPolicy   // qué hacer si ya hay uno con ese nombre
import androidx.work.PeriodicWorkRequestBuilder   // la petición «repite esto cada…»
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import java.util.concurrent.TimeUnit
 
// La tarea: lo que hay que hacer cada vez (igual que un worker de una sola vez).
class RegarWorker(contexto: Context, parametros: WorkerParameters) :
    CoroutineWorker(contexto, parametros) {
    override suspend fun doWork(): Result {
        Log.d("Plantas", "toca regar")
        return Result.success()
    }
}
 
fun programarRiego(contexto: Context) {
    // Periódica: «repite RegarWorker cada 3 días». El mínimo son 15 minutos.
    val peticion = PeriodicWorkRequestBuilder<RegarWorker>(3, TimeUnit.DAYS)
        .build()
    WorkManager.getInstance(contexto).enqueueUniquePeriodicWork(
        "riego",                            // el nombre único
        ExistingPeriodicWorkPolicy.KEEP,    // si ya hay uno, se queda el que había
        peticion,
    )
}

Tres ideas salen de ahí. La primera: el mínimo de un trabajo periódico son 15 minutos; si pides menos, WorkManager lo sube a 15 y lo apunta en el registro («Interval duration lesser than minimum allowed value; Changed to 900000», esos 900 000 son milisegundos). La segunda: el nombre único convierte «programar» en algo que puedes repetir sin miedo, y la política decide qué pasa cuando el nombre ya existe. Son tres:

PolíticaSi ya hay un trabajo con ese nombre
KEEPNo hace nada: se queda el que había, con su cuenta.
UPDATECambia la petición del que hay y sigue su cuenta.
CANCEL_AND_REENQUEUELo cancela y encola uno nuevo, que se ejecuta al momento y empieza la cuenta de cero.

La tercera: si no pones retraso inicial, la primera ejecución llega en cuanto lo encolas, y las siguientes una por periodo desde ese momento. Lo verás con la Pokédex en el paso 5. Si quieres provocar lo de las políticas tú, está al final de ese paso.

Paso 1 · Instala WorkManager

1 · Qué instalas y por qué ahora. androidx.work:work-runtime-ktx es WorkManager, la librería de Google para dejar trabajo en manos del sistema. Va en androidApp y no en shared, porque es una librería de Android: meterla en el módulo común obligaría a inventar un equivalente para iOS que no existe. En iOS esto se resuelve de otra manera, y esa es su lección.

2 · La versión, en [versions]. Abre gradle/libs.versions.toml y añade una línea al final de la sección [versions]:

gradle/libs.versions.toml: [versions], línea nueva
work = "2.12.0"    # una versión estable (sin -alpha, -beta ni -rc)

3 · La librería, en [libraries]. Al final de esa sección (la cabecera ya existe; no la repitas):

gradle/libs.versions.toml: [libraries], línea nueva
androidx-work-runtime = { module = "androidx.work:work-runtime-ktx", version.ref = "work" }

4 · No es un plugin, así que [plugins] y el build.gradle.kts de la raíz no se tocan.

5 · La dependencia, en el módulo que la usa. Abre androidApp/build.gradle.kts y, dentro del bloque dependencies { … }, al final:

androidApp/build.gradle.kts: línea nueva
    // implementation: solo la usa el código de esta app. El alias es el del
    // catálogo con los guiones hechos puntos.
    implementation(libs.androidx.work.runtime)

6 · Sync Now, y comprueba. Pulsa Sync Now. Si sale en rojo, repasa la caja «Si Sync sale en rojo» de Los ficheros de Gradle de tu proyecto (aquí: libs.androidx.work-runtime con guion en vez de punto, o un dígito de más en la versión). Para comprobar que llegó, pide a Gradle la lista de librerías con las que se ejecuta la versión de depuración de la app (debugRuntimeClasspath) y quédate con las líneas que contienen androidx.work (grep filtra líneas). En la Terminal, en la carpeta de tu proyecto:

./gradlew :androidApp:dependencies --configuration debugRuntimeClasspath | grep androidx.work

Debe aparecer androidx.work:work-runtime-ktx:2.12.0.

Paso 2 · El trabajo que enseña el Pokémon

El archivo. RecordatorioDiario.kt tendrá dos piezas: el trabajo en sí (qué hacer) y la función que lo programa (cuándo y cada cuánto). Va junto a Notificaciones.kt, en la carpeta daily.

Créalo. En la vista Project, clic derecho sobre la carpeta androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/daily → New → File y escribe RecordatorioDiario.kt. Plan B, desde la Terminal, en la carpeta de tu proyecto (touch no crea carpetas, por eso va antes mkdir -p; la carpeta daily ya existe y no pasa nada):

mkdir -p androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/daily
touch androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/daily/RecordatorioDiario.kt

Quién lo construye. Tú no escribes nunca RecordatorioDiarioWorker(contexto, parametros). Le dices a WorkManager la clase (PeriodicWorkRequestBuilder<RecordatorioDiarioWorker>, en el paso siguiente) y es él quien, cuando toca, crea el objeto con el Context de la aplicación y los WorkerParameters de esa ejecución. Por eso el constructor tiene que ser exactamente ese, con esos dos argumentos y en ese orden.

Cabecera y trabajo:

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/daily/RecordatorioDiario.kt: cabecera y el trabajo
package com.sebastiannarvaez.pokedex.android.daily
 
import android.content.Context
import androidx.work.CoroutineWorker                 // un trabajo que puede usar corrutinas
import androidx.work.ExistingPeriodicWorkPolicy      // qué hacer si ya hay uno con ese nombre
import androidx.work.PeriodicWorkRequestBuilder      // la petición «repite esto cada…»
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import com.sebastiannarvaez.pokedex.core.Log
import java.util.concurrent.TimeUnit
import kotlin.time.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime
 
/** La fecha de hoy en la zona del dispositivo, que es la que ve el usuario. */
fun hoy() = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
 
/**
 * El trabajo que muestra la notificación del día.
 * No pide nada a la red: el Pokémon del día es una función de la fecha, así que
 * este trabajo nunca falla por falta de conexión.
 */
class RecordatorioDiarioWorker(
    context: Context,
    parametros: WorkerParameters,
) : CoroutineWorker(context, parametros) {
 
    override suspend fun doWork(): Result {
        val fecha = hoy()
        Log.i("recordatorio diario para $fecha")    // una marca para ver que se ejecutó
        mostrarPokemonDelDia(applicationContext, fecha)
        return Result.success()                     // «hecho»; las otras: failure() y retry()
    }
}

Tres cosas de ese bloque:

  • hoy() es la frontera de la fecha que viste en El Pokémon del día en el núcleo: convierte «el instante de ahora» en «qué día es aquí», con la zona horaria del móvil, y se lo da ya resuelto a la regla. La escribes aquí, en daily, porque la van a usar dos sitios: este trabajo y la fila de Ajustes (paso 4). Así la conversión vive en un solo lugar y las dos pantallas no pueden discrepar.
  • Log es el de la Pokédex (com.sebastiannarvaez.pokedex.core.Log), el que escribe con la etiqueta Pokedex. No es el android.util.Log del ejemplo de las plantas, que pedía la etiqueta a mano.
  • El Result de doWork() es el de WorkManager, no el de Kotlin: la caja «Ojo» de Trabajo en segundo plano lo explica.

Ese comentario de «nunca falla por falta de conexión» tiene consecuencias: es la recompensa de la decisión que se tomó dos lecciones atrás. Un trabajo que necesitara red tendría que declarar una restricción, esperar a tenerla, tratar el fallo y reintentar con espera creciente. Este no puede fallar por eso, así que no hay nada de eso que escribir.

Paso 3 · Programarlo, con dos decisiones dentro

Al final del mismo fichero:

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/daily/RecordatorioDiario.kt: programarlo
const val NOMBRE_TRABAJO = "recordatorio-diario"   // el nombre único del trabajo
 
fun programarRecordatorioDiario(context: Context) {
    // «Repite el trabajo cada 1 día».
    val trabajo = PeriodicWorkRequestBuilder<RecordatorioDiarioWorker>(1, TimeUnit.DAYS)
        .build()
 
    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        NOMBRE_TRABAJO,                       // con nombre: no puede haber dos iguales
        ExistingPeriodicWorkPolicy.KEEP,      // si ya existe, déjalo como está
        trabajo,
    )
}

(JobScheduler es el planificador de trabajos de Android, la lista del sistema donde WorkManager deja lo que programas; lo miras tú mismo en el paso 5.) La documentación dice, en general, que si el trabajo no tiene restricciones el sistema puede ejecutarlo enseguida. Con un trabajo periódico sin retraso inicial es lo que pasa: la primera ejecución llega en cuanto lo encolas (lo verás en el paso 5), y las siguientes, una por periodo desde entonces. Así que el aviso no cae «a las nueve de la mañana», sino a la hora a la que se activó. Para fijar una hora habría que calcular un setInitialDelay hasta la próxima, y aun así el sistema puede moverla unos minutos. Para un recordatorio diario sobra; para algo que tenga que sonar a una hora exacta haría falta una alarma.

La segunda decisión, KEEP, es de las que se rompen sin querer en un refactor: si ya hay un trabajo con ese nombre, no hace nada. CANCEL_AND_REENQUEUE cancela el pendiente y encola uno nuevo, que se ejecuta al momento: con ella, cada toque enseñaría el aviso y reiniciaría la cuenta de veinticuatro horas. UPDATE cambia la petición sin reiniciar la cuenta: es la que usarías si un día cambias el periodo, porque KEEP conservaría el trabajo viejo. Lo probarás en el paso 5.

Paso 4 · Programar al activar

Abre androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/settings/SettingsScreen.kt. Son tres cambios, y los dos primeros son de limpieza. Primero, los imports, arriba del fichero, junto a los demás:

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/settings/SettingsScreen.kt: imports nuevos
import com.sebastiannarvaez.pokedex.android.daily.hoy
import com.sebastiannarvaez.pokedex.android.daily.programarRecordatorioDiario

Segundo: la conversión de la fecha ya no vive aquí. Borra del final del fichero la función privada hoy() que escribiste en El permiso de notificaciones en Android (la que empieza por private fun hoy() = Clock.System…) y, con ella, los tres imports que solo usaba: kotlin.time.Clock, kotlinx.datetime.TimeZone y kotlinx.datetime.toLocalDateTime. Si no los borras no pasa nada grave (Android Studio los pinta en gris), pero con dos hoy() a la vista el fichero confunde. Ahora la única es la de daily, que importaste arriba.

Tercero, la llamada. En la fila «Pokémon del día» que creaste en esa misma lección, el alPulsar tiene un if (permiso == EstadoDelPermiso.CONCEDIDO) { … } else { … }. Sustituye ese if entero por este (pierde el comentario que tenía):

androidApp/src/main/kotlin/com/sebastiannarvaez/pokedex/android/settings/SettingsScreen.kt: la llamada nueva
                    if (permiso == EstadoDelPermiso.CONCEDIDO) {
                        // Programar es idempotente con KEEP: tocar aquí más de una
                        // vez no duplica nada.
                        programarRecordatorioDiario(contexto)
                        mostrarPokemonDelDia(contexto, hoy())
                    } else {
                        pedirPermiso()
                    }

Dos líneas seguidas que hacen cosas distintas. programarRecordatorioDiario(contexto) deja la nota en el mostrador del sistema; mostrarPokemonDelDia(contexto, hoy()) enseña la notificación ahora mismo. La primera vez que tocas, el trabajo periódico ya se ejecuta solo (lo dijo la documentación) y la segunda línea publica el mismo aviso, que sustituye al anterior (los dos usan el mismo número de notificación, así que no se apilan). En los toques siguientes KEEP no ejecuta nada, y es la segunda línea la que te deja ver la notificación cuando quieras. Por eso la app separa las dos preguntas: «¿se ve la notificación?» se responde ahora, y «¿está programada?» se responde con el dumpsys del paso siguiente. Mezcladas, un fallo de la segunda parecería un fallo de la primera.

Paso 5 · Cómo se comprueba que está programado

Ejecuta la app, ve a Ajustes y concede el permiso si hace falta. Todos los comandos se escriben en la Terminal de Android Studio. Hace falta un emulador con Android 14 o superior (nivel de API 34): la parte 3 usa los espacios de nombres del planificador, que llegaron con esa versión. Las pruebas están hechas en Android 15.

1 · Que el trabajo se ejecutó. Primero vacía el registro (-c); luego toca la fila Pokémon del día en el móvil; luego léelo (-d), filtrando por la etiqueta que usa Log (-s Pokedex):

adb logcat -c

(Toca ahora la fila en el móvil. Después:)

adb logcat -d -s Pokedex

Debe salir una línea como I Pokedex : recordatorio diario para 2026-10-01 (con la fecha de hoy). Es la primera ejecución, inmediata. Ahora vacía el registro otra vez, toca la fila otra vez y vuelve a leerlo:

adb logcat -c

(Toca la fila otra vez en el móvil. Después:)

adb logcat -d -s Pokedex

No sale ninguna línea nueva. Eso es KEEP: no se ha encolado nada. (Sin el -c verías la línea de antes y no sabrías si es nueva.)

Un aviso, por si ya tocaste la fila antes de llegar aquí: KEEP no escribe nada si ya existe un trabajo con ese nombre, así que el que hay es el que se programó entonces. Si cambias algo del trabajo (el periodo, por ejemplo), no se aplica hasta que lo canceles o desinstales la app. Para empezar limpio, desinstala la app del emulador y vuelve a ejecutarla.

2 · Que está encolado para mañana. jobscheduler es el planificador de trabajos de Android: la lista donde el sistema apunta qué trabajos tiene pendientes y cuándo puede lanzarlos. WorkManager no ejecuta nada por su cuenta: le deja ahí sus trabajos. dumpsys imprime el estado interno de un servicio del sistema. Filtra por tu app (el grep va entre comillas dentro del comando para que se ejecute en el móvil y no en tu ordenador):

adb shell "dumpsys jobscheduler | grep -A 9 'JOB androidx.work.systemjobscheduler:u0a.*@com.sebastiannarvaez.pokedex'"

El patrón dice: una línea que empiece por JOB androidx.work.systemjobscheduler:u0a, siga con cualquier cosa (.*) y acabe en @com.sebastiannarvaez.pokedex; -A 9 imprime además las nueve líneas siguientes. Lo que sale es más largo; esto es lo que importa (los … son líneas que aquí no importan):

  JOB androidx.work.systemjobscheduler:u0a208/0: fdf1893 @androidx.work.systemjobscheduler@com.sebastiannarvaez.pokedex/androidx.work.impl.background.systemjob.SystemJobService
    …
    Source: uid=u0a208 user=0 pkg=com.sebastiannarvaez.pokedex
    JobInfo:
      …
      Requires: charging=false batteryNotLow=false deviceIdle=false
      …
      Minimum latency: +23h59m59s956ms

Dos cosas que leer. androidx.work.systemjobscheduler es un espacio de nombres (namespace) que WorkManager reserva para sus trabajos: no es el nombre que pusiste, que vive en la base de datos de WorkManager. Y Minimum latency: +23h59m… es el «hasta dentro de un día» que pediste. El número tras la barra (0 en una instalación nueva) y el u0a208 cambian de un móvil a otro y de una ejecución a otra: usa los tuyos.

3 · Forzarlo, y por qué WorkManager se niega. El paso siguiente parece obvio: forzar la ejecución para no esperar un día. cmd habla con servicios del sistema, y cmd jobscheduler run -f fuerza un trabajo concreto. Con el número de tu dumpsys (aquí, 0):

adb shell cmd jobscheduler run -f com.sebastiannarvaez.pokedex 0
Could not find job 0 in package com.sebastiannarvaez.pokedex / user 0

No es que el trabajo no esté programado: acabas de verlo en el dumpsys. Es que vive en el espacio de nombres de WorkManager, y el comando busca en el espacio sin nombre. Se le dice con -n. Vacía el registro antes, para leer solo lo de esta ejecución. Y fíltralo por tu app: en el móvil hay otras apps que también usan WorkManager y llenarían la pantalla de líneas que no son tuyas. --pid deja solo las líneas del proceso de tu app, y pidof da su número, como en Un recordatorio con WorkManager. Lo de $( … ) significa «ejecuta primero lo de dentro y pega aquí lo que escriba»: aquí, el número del proceso de tu app. WM-WorkerWrapper es la etiqueta con la que WorkManager escribe en el registro cuando va a ejecutar un trabajo:

adb logcat -c
adb shell cmd jobscheduler run -f -n androidx.work.systemjobscheduler com.sebastiannarvaez.pokedex 0
adb logcat -d --pid=$(adb shell pidof com.sebastiannarvaez.pokedex) -s WM-WorkerWrapper
Running job [FORCED]
D WM-WorkerWrapper: Delaying execution for com.sebastiannarvaez.pokedex.android.daily.RecordatorioDiarioWorker because it is being executed before schedule.
D WM-WorkerWrapper: Status for … is ENQUEUED; not doing any work and rescheduling for later execution

(La primera línea la imprime cmd, no el registro.) Esas líneas salen con la configuración por defecto de WorkManager en una app de depuración: no hace falta tocar ningún nivel de registro.

Ahora el planificador sí obedece, pero WorkManager se niega: sabe que el periodo aún no se ha cumplido y, como dice la segunda línea, «no hace nada y lo reprograma para más tarde». Así protege la regla de «una vez por periodo», y es la razón de que un trabajo periódico no se pueda adelantar desde fuera. WorkManager tiene herramientas de prueba propias para eso (se usan desde tests instrumentados, que este curso no cubre); aquí la comprobación completa son estos tres pasos.

Una trampa: si repites el comando con el mismo número, falla. Tras cada ejecución forzada, WorkManager vuelve a programar el trabajo con otro número (0, luego 1, luego 2…), y el comando con el viejo responde «Could not find job 0 in package … / namespace androidx.work.systemjobscheduler / user 0». Si lo repites, vuelve a mirar el número en el dumpsys.

4 · Y si el proceso muere. El trabajo vive en el planificador, no en tu app. La prueba de matar el proceso con am kill y ver cómo el sistema lo resucita es la de Un recordatorio con WorkManager; vale igual aquí.

5 · Opcional, provócalo. Cambia KEEP por CANCEL_AND_REENQUEUE en programarRecordatorioDiario, vacía el registro (adb logcat -c) y toca la fila dos veces: en adb logcat -d -s Pokedex salen dos líneas «recordatorio diario para…», una por toque, porque cada llamada cancela, encola y ejecuta. Es lo que le pasaría al usuario si la app programara el recordatorio en cada arranque: vería el aviso cada vez que abre la app, a cualquier hora, y el «diario» dejaría de serlo sin ninguna señal de fallo. Devuelve KEEP al terminar.

Paso 6 · Lo que no hace falta y por eso no está

Hay tres cosas que suelen aparecer en código de segundo plano y que aquí no están, cada una por un motivo concreto:

  • Restricciones (setConstraints). No hay red que esperar ni batería que cuidar: el trabajo es una multiplicación y una notificación.
  • Reintentos (setBackoffCriteria). doWork() no tiene forma de fallar, así que devuelve Result.success() siempre.
  • Un servicio en primer plano. El trabajo dura milisegundos. Un servicio en primer plano exige una notificación permanente y un tipo declarado; sería pagar mucho por nada.

Y la contrapartida de programar existe cuando haga falta: WorkManager.getInstance(context).cancelUniqueWork(NOMBRE_TRABAJO) cancela por nombre, no por identificador. El identificador cambia cada vez que se encola; el nombre, no. La app todavía no ofrece desactivar el recordatorio, así que nada la llama y no la hemos escrito.

Paso 7 · Comprueba los tests

Esta lección no añade tests: siguen 80 en la JVM, 44 en Android host y 47 en iOS, los de El Pokémon del día en el núcleo. En la Terminal, sin filtrar, para que corran las tres plataformas:

./gradlew :shared:jvmTest :shared:testAndroidHostTest :shared:iosSimulatorArm64Test
open shared/build/reports/tests/jvmTest/index.html

Termina en BUILD SUCCESSFUL y la página enseña arriba el recuadro «tests» con 80; repite el open con testAndroidHostTest (44) y iosSimulatorArm64Test (47). Ojo: si pasas --tests a Gradle para correr solo una clase, el informe cuenta solo esas, y la cifra será menor (un filtro con la clase del Pokémon del día deja 4).

Punto de control

  • gradle/libs.versions.toml tiene work = "2.12.0" en [versions] y androidx-work-runtime en [libraries], y androidApp/build.gradle.kts tiene implementation(libs.androidx.work.runtime). Sync Now terminó sin errores rojos.
  • En daily está RecordatorioDiario.kt (con hoy(), el trabajo y programarRecordatorioDiario), y SettingsScreen.kt ya no tiene su propia hoy() privada y no tiene nada en rojo.
  • Tocar «Pokémon del día» con el permiso dado muestra la notificación al instante y deja recordatorio diario para <hoy> en adb logcat -d -s Pokedex.
  • Tocarla otra vez no añade ninguna línea nueva (KEEP).
  • dumpsys jobscheduler enseña un trabajo androidx.work.systemjobscheduler de tu app con Minimum latency: +23h59m….
  • cmd jobscheduler run -f sin -n responde «Could not find job…»; con -n, el registro de tu app dice que se ejecuta antes de tiempo y lo reprograma, y el dumpsys enseña un número nuevo.
  • Sin tests nuevos: siguen 80 / 44 / 47.

Comprueba lo que entendiste

8 preguntas · necesitas 6 para completar la lección

  1. 1.¿Cada cuánto puede repetirse como mínimo una tarea periódica de WorkManager?

  2. 2.Con CANCEL_AND_REENQUEUE en vez de KEEP, ¿qué pasa al tocar la fila dos veces?

  3. 3.cmd jobscheduler run -f <paquete> 3 responde «Could not find job 3 in package…», pero el trabajo sale en dumpsys. ¿Por qué?

  4. 4.¿Por qué el worker no declara ninguna restricción de red?

  5. 5.Un trabajo periódico se encola sin retraso inicial. ¿Cuándo se ejecuta por primera vez?

  6. 6.Has forzado el trabajo con -n y repites el mismo comando con el mismo número. Responde «Could not find job…». ¿Por qué?

  7. 7.Cambias el trabajo para que se repita cada 12 horas en vez de cada 24. Con KEEP, ¿qué pasa en un móvil que ya lo tenía programado?

  8. 8.La función hoy() está en daily/RecordatorioDiario.kt y la usan el trabajo y la fila de Ajustes. ¿Qué se gana con eso?

Faltan 8 de 8