Botón Back y Handler en Android: pulsar dos veces
El botón atrás funciona como la puerta de una habitación: por defecto, al pulsarlo sales a la pantalla anterior. A veces quieres que la app pregunte «¿seguro que te vas?» antes de dejarte salir.
La forma antigua: sobrescribir onBackPressed
En 2018 se hacía sobrescribiendo el método de la activity:
override fun onBackPressed() {
super.onBackPressed() // si quitas esta línea, el botón atrás deja de funcionar en esta pantalla
}
Pulsar dos veces para salir, versión actual
La idea es la misma que en el artículo original: la primera pulsación avisa y la segunda, si llega antes de 2 segundos, sale.
[SIN EJECUTAR] El código de la activity necesita un proyecto de Android y no se ha ejecutado en esta revisión: sigue la API documentada de AndroidX Activity (1.13.0, la última estable en noviembre de 2026). La regla de los 2 segundos sí se ha ejecutado, separada en Kotlin puro, más abajo.
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.widget.Toast
import androidx.activity.addCallback
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
private val handler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
onBackPressedDispatcher.addCallback(this) {
// Primera pulsación: avisamos y dejamos de interceptar
isEnabled = false
Toast.makeText(
this@MainActivity,
"Pulsa atrás otra vez para salir de esta pantalla",
Toast.LENGTH_SHORT
).show()
// A los 2 segundos volvemos a interceptar
handler.postDelayed({ isEnabled = true }, 2000)
}
}
override fun onDestroy() {
handler.removeCallbacksAndMessages(null) // no dejes tareas pendientes al cerrar
super.onDestroy()
}
}
Cómo funciona
Primera pulsación:
- El callback está activo, así que Android le pasa la pulsación.
- Se desactiva (
isEnabled = false) y muestra elToast. - El
Handlerprograma que se vuelva a activar en 2 segundos.
Si vuelves a pulsar antes de 2 segundos:
- El callback está desactivado, así que Android hace lo de siempre: vuelve a la pantalla anterior, con su animación.
Si pasan los 2 segundos sin pulsar:
- El
HandlerejecutaisEnabled = truey todo vuelve a empezar.
Qué se corrigió del código original
| Original (2018) | Ahora | Por qué |
|---|---|---|
override fun onBackPressed() |
onBackPressedDispatcher.addCallback |
onBackPressed está obsoleto desde la API 33 |
Handler() |
Handler(Looper.getMainLooper()) |
El constructor sin argumentos está obsoleto: elegir el hilo de forma implícita provoca errores |
Variable exitEnabled |
isEnabled del propio callback |
El callback ya guarda si está activo |
Tras super.onBackPressed() seguía ejecutándose y mostraba el Toast |
Un único camino por pulsación | Faltaba un return dentro del if |
Si quieres entender qué le pasa a la activity cuando sales de ella, mira El ciclo de vida de las activities en Android. El Toast que usamos está explicado en Toast y Snackbar en Android.
Qué cambia con Android 16
Hasta ahora, sobrescribir onBackPressed() era una forma antigua pero que todavía funcionaba en la mayoría de las apps. Desde Android 13, la referencia de Activity dice que ni onBackPressed() ni KEYCODE_BACK deben usarse para gestionar el botón atrás, y una app que activaba el gesto predictivo a mano (android:enableOnBackInvokedCallback="true") ya dependía de los callbacks. Con Android 16 (API 36) eso pasa a ser lo normal: si tu app apunta a esa versión (targetSdk = 36) y se ejecuta en un móvil con Android 16 o posterior, la documentación de Android dice que onBackPressed ya no se llama y que el evento de tecla KEYCODE_BACK ya no llega. Además, las animaciones del gesto atrás predictivo (volver al inicio, cambiar de tarea o de activity) se activan por defecto.
Es decir: el código de 2018 sigue compilando, solo con un aviso de método obsoleto, pero el botón atrás deja de pasar por él. El fallo no lo ves al compilar, sino al probar en un móvil nuevo.
Si no puedes migrar todavía, Google permite desactivarlo de forma temporal con android:enableOnBackInvokedCallback="false" en el <application> o en una <activity> del AndroidManifest.xml. Es un parche: lo recomendado es pasar a OnBackPressedCallback, que funciona con ese valor o sin él.
Separa la regla para poder probarla
La parte delicada del ejemplo no es Android, es la regla: «después de avisar, deja salir durante 2 segundos». Si esa regla vive dentro de la activity, solo la puedes probar en un emulador y pulsando a mano. Si la sacas a una clase de Kotlin normal, la puedes probar en milisegundos.
El truco es no leer la hora directamente, sino recibir un reloj: una función que devuelve la hora actual. En la app le pasas el reloj de verdad; en el test, uno que controlas tú. Es como ensayar una obra con un reloj de atrezo: puedes saltar a «dos segundos después» sin esperar.
class AvisoDeSalida(
private val ventanaMs: Long = 2000,
private val reloj: () -> Long,
) {
private var avisadoEn: Long? = null
fun avisar() {
avisadoEn = reloj()
}
// ¿Debe el callback interceptar la próxima pulsación?
val debeInterceptar: Boolean
get() {
val aviso = avisadoEn ?: return true
return reloj() - aviso >= ventanaMs
}
}
Y su comprobación, con kotlin.test, la biblioteca de aserciones de Kotlin. Va en el mismo fichero AvisoDeSalida.kt, pero los dos import tienen que ir arriba del todo, antes de la clase: Kotlin no admite import en mitad de un fichero.
import kotlin.test.assertFalse
import kotlin.test.assertTrue
fun main() {
var ahora = 0L
val aviso = AvisoDeSalida(reloj = { ahora })
assertTrue(aviso.debeInterceptar, "al abrir la pantalla, intercepta")
aviso.avisar()
assertFalse(aviso.debeInterceptar, "justo después del aviso, deja salir")
ahora = 1_999
assertFalse(aviso.debeInterceptar, "a 1,999 s todavía deja salir")
ahora = 2_000
assertTrue(aviso.debeInterceptar, "a los 2 s vuelve a interceptar")
println("4 comprobaciones superadas")
}
kotlinc AvisoDeSalida.kt -cp kotlinc/lib/kotlin-test.jar -include-runtime -d aviso.jar
java -cp aviso.jar:kotlinc/lib/kotlin-test.jar AvisoDeSalidaKt
4 comprobaciones superadas
Si alguien cambia >= por >, la diferencia es de un milisegundo y a mano nadie la notaría. El test, sí:
Exception in thread "main" java.lang.AssertionError: a los 2 s vuelve a interceptar
En la activity, el callback solo traduce la regla a Android. [SIN EJECUTAR]:
// import android.os.SystemClock
// aviso es una propiedad de MainActivity; el addCallback va dentro de onCreate
private val aviso = AvisoDeSalida(reloj = { SystemClock.uptimeMillis() })
onBackPressedDispatcher.addCallback(this) {
aviso.avisar()
isEnabled = aviso.debeInterceptar // false: la siguiente pulsación sale
Toast.makeText(this@MainActivity, "Pulsa atrás otra vez para salir", Toast.LENGTH_SHORT).show()
handler.postDelayed({ isEnabled = aviso.debeInterceptar }, 2000)
}
SystemClock.uptimeMillis() es el mismo reloj que usa Handler.postDelayed, según su referencia. En un proyecto de Android, el test iría en src/test con JUnit; aquí se ha ejecutado con kotlinc para no depender de Android. Es la misma idea que pide Google en su guía del gesto atrás: decidir a partir de un estado cuándo está activo cada callback, en vez de reaccionar con condicionales cuando la pulsación ya ha ocurrido.
Errores frecuentes
- Registrar el callback sin
this.addCallback(this) { … }ata el callback al ciclo de vida de la activity o del fragment: solo actúa cuando la pantalla está activa y se elimina sola al destruirse. Sin elLifecycleOwner, tienes que quitarlo tú conremove(); si no, en un fragment sigue interceptando cuando el fragment ya no está. - Llamar a
onBackPressedDispatcher.onBackPressed()dentro del callback sin desactivarlo antes. El dispatcher entrega la pulsación al último callback activo, que es el mismo: se llama a sí mismo una y otra vez. Pon antesisEnabled = false. - Usar el callback para registrar analíticas. Mientras hay un callback activo, Android no muestra la animación predictiva y tu código se encarga de la pulsación. Google pide no usar estos callbacks para lógica de negocio ni para registrar eventos. Para saber si el usuario sale de una activity, comprueba
isFinishingenonDestroy(). - Varios callbacks a la vez. Si añades varios, responde el último que añadiste y que esté activo; los demás solo actúan si los de encima están desactivados. Para cambios temporales, la documentación recomienda activar y desactivar (
isEnabled) en lugar de quitar y volver a añadir, porque así se conserva el orden. - Dejar el
Handlersin limpiar. Si la activity se cierra con una tarea pendiente, elHandlerla ejecuta igual. Por eso el ejemplo llama aremoveCallbacksAndMessages(null)enonDestroy().
Cuándo no usarlo
- Para todo. El usuario espera que atrás funcione a la primera. Un aviso tiene sentido cuando salir le hace perder algo: un formulario a medias, un pago en curso. En una pantalla de inicio sin nada que perder, la segunda pulsación es una molestia.
- Si haces la pantalla con Jetpack Compose. Ahí no se usa el dispatcher a mano: Compose tiene
BackHandler(enabled = …) { … }y, si quieres seguir el progreso del gesto para animarlo,PredictiveBackHandler. - Para navegar entre pantallas de la app. Si usas Fragments o el componente Navigation, ya gestionan la pila de pantallas con el dispatcher. Basta con usar AndroidX Activity 1.6.0 o posterior; no hace falta interceptar nada.
Ejercicio de cinco minutos
- Cambia la ventana a 3 segundos. El test crea
AvisoDeSalidasinventanaMs, así que usa los 2 segundos por defecto. Escribe primero las comprobaciones con 2999 y 3000 ms y comprueba que la de 2999 falla; después pasaventanaMs = 3000o cambia el valor por defecto. - Añade un método
reiniciar()que vuelva al estado inicial y pruébalo. - Busca en tu proyecto
override fun onBackPressed. Cada uno que encuentres deja de llamarse cuando la app apunte a Android 16 y se ejecute en un móvil con Android 16 o posterior: pásalo aaddCallback.
Fuentes
Consultadas el 11 de noviembre de 2026.
- Android Developers: Add support for the predictive back gesture: migrar a
OnBackPressedCallback(AndroidX Activity 1.6.0 o posterior),enableOnBackInvokedCallback,BackHandleryPredictiveBackHandleren Compose, decidir el estado que activa cada callback, que un callback activo impide la animación predictiva y que no se usen para lógica de negocio ni para registrar eventos (isFinishingenonDestroy()). - Android Developers: Provide custom back navigation:
addCallbackconLifecycleOwner(solo con la pantalla enSTARTED, eliminación automática al destruirse), orden inverso de los callbacks, solo los activos reciben la pulsación y preferencia porisEnabledpara cambios temporales. - Android Developers: Behavior changes: apps targeting Android 16: con
targetSdk36 en Android 16,onBackPressedno se llama,KEYCODE_BACKno se envía y las animaciones predictivas están activas por defecto; desactivación temporal conandroid:enableOnBackInvokedCallback="false". - Android Developers: OnBackPressedDispatcher (AndroidX Activity), Handler (constructor
Handler()obsoleto) y notas de versión de AndroidX Activity (1.13.0, última estable; desde la 1.12.0,OnBackPressedDispatcherse apoya en NavigationEvent, y la 1.12.0-alpha01, del 20 de mayo de 2025, recomiendaNavigationEventDispatcheryNavigationEventCallback). - La obsolescencia de
Activity.onBackPressed()en la API 33 figura en la referencia de Activity, que añade que desde Android 13 nionBackPressed()niKEYCODE_BACKdeben usarse para gestionar el botón atrás. - Código:
AvisoDeSaliday sus comprobaciones se han compilado y ejecutado el 11 de noviembre de 2026 conkotlinc-jvm2.4.21 ykotlin-test2.4.21 (zip oficial de GitHub) sobre OpenJDK 25.0.5, en la imagen Dockereclipse-temurin:25. El código de la activity está[SIN EJECUTAR]: sigue la API documentada de AndroidX Activity.