Nivel inicial12 min de lectura · Publicado el

Botón Back y Handler en Android: pulsar dos veces

Botón atrás en Android: la primera pulsación avisa, la segunda antes de 2 segundos sale y el Handler reactiva el callback. Atrás dos veces para salirrubentejera.com 0 s2 sisEnabled = false1.ª: aviso2.ª: salesHandler
La primera pulsación muestra el aviso y desactiva el callback (isEnabled = false). Si vuelves a pulsar antes de 2 segundos, sales de la pantalla; si no, el Handler lo reactiva y todo vuelve a empezar.

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 el Toast.
  • El Handler programa 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 Handler ejecuta isEnabled = true y 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 el LifecycleOwner, tienes que quitarlo tú con remove(); 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 antes isEnabled = 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 isFinishing en onDestroy().
  • 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 Handler sin limpiar. Si la activity se cierra con una tarea pendiente, el Handler la ejecuta igual. Por eso el ejemplo llama a removeCallbacksAndMessages(null) en onDestroy().

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

  1. Cambia la ventana a 3 segundos. El test crea AvisoDeSalida sin ventanaMs, 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 pasa ventanaMs = 3000 o cambia el valor por defecto.
  2. Añade un método reiniciar() que vuelva al estado inicial y pruébalo.
  3. 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 a addCallback.

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, BackHandler y PredictiveBackHandler en 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 (isFinishing en onDestroy()).
  • Android Developers: Provide custom back navigation: addCallback con LifecycleOwner (solo con la pantalla en STARTED, eliminación automática al destruirse), orden inverso de los callbacks, solo los activos reciben la pulsación y preferencia por isEnabled para cambios temporales.
  • Android Developers: Behavior changes: apps targeting Android 16: con targetSdk 36 en Android 16, onBackPressed no se llama, KEYCODE_BACK no se envía y las animaciones predictivas están activas por defecto; desactivación temporal con android: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, OnBackPressedDispatcher se apoya en NavigationEvent, y la 1.12.0-alpha01, del 20 de mayo de 2025, recomienda NavigationEventDispatcher y NavigationEventCallback).
  • La obsolescencia de Activity.onBackPressed() en la API 33 figura en la referencia de Activity, que añade que desde Android 13 ni onBackPressed() ni KEYCODE_BACK deben usarse para gestionar el botón atrás.
  • Código: AvisoDeSalida y sus comprobaciones se han compilado y ejecutado el 11 de noviembre de 2026 con kotlinc-jvm 2.4.21 y kotlin-test 2.4.21 (zip oficial de GitHub) sobre OpenJDK 25.0.5, en la imagen Docker eclipse-temurin:25. El código de la activity está [SIN EJECUTAR]: sigue la API documentada de AndroidX Activity.