Cómo brindar navegación de retroceso personalizada

Los usuarios navegan hacia atrás por las pantallas con la navegación hacia atrás. La mayoría de los dispositivos Android tienen un botón Atrás (físico, de software o basado en gestos). Por lo general, no debes agregar un botón Atrás a tu app. Sin embargo, los dispositivos Android Automotive OS (AAOS) en modo de compatibilidad usan un botón Atrás del sistema. Esto controla la navegación, por lo que no necesitas agregar la tuya. Para obtener más detalles, consulta Modo de compatibilidad de AAOS.

Android mantiene una pila de actividades de destinos a medida que el usuario navega por tu aplicación. Por lo general, esto permite que Android navegue correctamente a destinos anteriores cuando se presiona el botón Atrás. Sin embargo, hay algunos casos en los que tu app podría necesitar implementar su propio comportamiento del botón Atrás a fin de brindar una mejor experiencia del usuario. Por ejemplo, cuando usas un objeto WebView, es posible que te convenga anular el comportamiento predeterminado del botón Atrás de modo que se le permita al usuario navegar hacia atrás en su historial de navegación web en lugar de las pantallas anteriores en tu app.

Cómo implementar la navegación hacia atrás personalizada en Compose

En Jetpack Compose, puedes controlar la navegación hacia atrás personalizada con el elemento componible BackHandler.

Cuando usas Navigation Compose, por lo general, usas NavController.navigateUp() o NavController.popBackStack() para navegar a la pantalla anterior en la pila de actividades. Sin embargo, BackHandler es útil para los casos en los que deseas implementar un comportamiento personalizado cuando el usuario presiona el botón Atrás del sistema o usa el gesto atrás. Por ejemplo, si muestras un WebView en tu app, es posible que quieras permitir que los usuarios naveguen hacia atrás por el historial de navegación cuando presionen el botón Atrás del sistema.

Uso básico

El siguiente es un ejemplo que muestra cómo interceptar el evento atrás para controlar el estado personalizado (como contraer un panel):

@Composable
fun MyScreen() {
    var isExpanded by remember { mutableStateOf(false) }

    // Intercept the back button or gesture only when isExpanded is true
    BackHandler(enabled = isExpanded) {
        // Collapsing the content disables this handler, so it does not
        // receive the next back event.
        isExpanded = false
    }

    if (isExpanded) {
        Text("Expanded content. Go back to collapse it.")
    } else {
        Button(onClick = { isExpanded = true }) {
            Text("Expand")
        }
    }
}

Intercepción de árbol componible

Si tienes varios elementos componibles BackHandler habilitados en diferentes niveles de tu árbol componible, solo el más interno intercepta el evento atrás.

Cómo implementar la navegación hacia atrás personalizada con Views

ComponentActivity, la clase base para FragmentActivity y AppCompatActivity, te permite controlar el comportamiento del botón Atrás con su OnBackPressedDispatcher, que puedes recuperar llamando a getOnBackPressedDispatcher().

El OnBackPressedDispatcher controla cómo se envían los eventos del botón Atrás a uno o más objetos OnBackPressedCallback. El constructor de OnBackPressedCallback toma un valor booleano para el estado habilitado inicial. Solo cuando se habilita una devolución de llamada, por ejemplo, cuando isEnabled() muestra true, el dispatcher llamará a handleOnBackPressed() de la devolución de llamada para controlar el evento del botón Atrás. Puedes cambiar el estado habilitado llamando a setEnabled().

Las devoluciones de llamada se agregan a través de los métodos addCallback. Usa el addCallback() método que toma un LifecycleOwner. De esta manera, el OnBackPressedCallback solo se agrega cuando el LifecycleOwner es Lifecycle.State.STARTED. La actividad también quita las devoluciones de llamada registradas cuando se destruye su objeto LifecycleOwner asociado, lo que evita fugas de memoria y permite que se lo utilice en fragmentos y otros propietarios del ciclo de vida con tiempos de vida más cortos que la actividad.

El siguiente es un ejemplo de implementación de devolución de llamada:

class MyFragment : Fragment() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // This callback will only be called when MyFragment is at least Started.
        val callback = requireActivity().onBackPressedDispatcher.addCallback(this) {
            // Handle the back button event
        }

        // The callback can be enabled or disabled here or in the lambda
    }
    ...
}

Puedes proporcionar varias devoluciones de llamada con addCallback(). Cuando lo haces, las devoluciones de llamada se invocan en el orden inverso en el que se agregan: la devolución de llamada agregada en último lugar es la primera que tiene la oportunidad de controlar el evento del botón Atrás. Por ejemplo, si agregaste tres devoluciones de llamada llamadas one, two y three en orden, se invocarían en el orden de three, two y one, respectivamente.

Las devoluciones de llamadas siguen el patrón de la Cadena de responsabilidad. Cada devolución de llamada en la cadena se invoca solo si la devolución de llamada anterior no estaba habilitada. Esto significa que, en el ejemplo anterior, la devolución de llamada two se invocaría solo si la devolución de llamada three no estaba habilitada. La devolución de llamada one solo se invocaría si la devolución de llamada two no estaba habilitada, y este patrón continúa para cualquier devolución de llamada adicional.

Ten en cuenta que, cuando se agrega con addCallback(), la devolución de llamada no se agrega a la cadena de responsabilidad hasta que el LifecycleOwner pasa al estado Lifecycle.State.STARTED.

Se recomienda cambiar el estado habilitado en la devolución de llamada para los cambios temporales, ya que mantiene el orden descrito anteriormente, lo que es particularmente importante si tienes devoluciones de llamada registradas en varios propietarios del ciclo de vida anidados diferentes.

Si deseas quitar la devolución de llamada por completo, llama a remove(). Por lo general, esto no es necesario porque las devoluciones de llamada se quitan automáticamente cuando se destruye su LifecycleOwner asociado.