Saltar al contenido
Todas las entradas

Edge-to-edge y retroceso predictivo en Android en 2026: qué es obligatorio ahora y cómo no romper tu interfaz

El edge-to-edge ahora es obligatorio en API 35+ y el retroceso predictivo es el gesto por defecto. Una guía práctica sobre insets de Compose, WindowInsets y PredictiveBackHandler.

MFKAPPS 6 min de lectura

Dos cambios de interfaz de Android dejaron de ser opcionales en este ciclo. Las apps que apuntan a API 35+ tienen el edge-to-edge forzado — ya no puedes optar por que el sistema dibuje barras opacas por ti. Y el retroceso predictivo, el gesto de deslizar hacia atrás que previsualiza a dónde vas antes de confirmarlo, ahora es la interacción por defecto en dispositivos con navegación por gestos, en lugar de una función experimental. Ninguno de los dos es un rediseño. Ambos van a romper visiblemente una interfaz que no fue construida para ellos, y el modo de fallo es el mismo en ambos casos: funciona bien en el emulador que probaste y se ve roto en un teléfono real con un notch o con la navegación de tres botones desactivada.

Esto es lo que realmente cambió, y el código de Compose que mantiene correctas las pantallas de cinco apps bajo ambos cambios.

El edge-to-edge ya no es una opción

Antes de la API 35, enableEdgeToEdge() era algo que activabas para una pantalla específica — un extra agradable para apps que querían que el contenido fluyera bajo una barra de estado translúcida. Apuntar a la API 35 elimina esa opción: el comportamiento de Window.setDecorFitsSystemWindows(false) se fuerza, las barras del sistema se vuelven transparentes, y tu contenido raíz se dibuja detrás de ellas, hayas llamado a algo o no.

El efecto práctico: cualquier diseño que asumiera que la barra de estado y la barra de navegación reservaban su propio espacio ahora ve el contenido empezando en el píxel físico cero. El título de una barra de app superior queda debajo del reloj. Los iconos de una barra de navegación inferior quedan debajo de la píldora del gesto. Esto no es un problema de acabado visual — en un dispositivo real significa que los usuarios no pueden tocar la fila inferior de tu interfaz.

La solución no es “añadir padding en todas partes”. Es aplicar WindowInsets en el límite correcto, una sola vez, para que las barras del sistema reserven espacio exactamente donde el contenido lo necesita, y en ningún otro lugar.

setContent {
    AppTheme {
        Scaffold(
            contentWindowInsets = WindowInsets.safeDrawing,
            topBar = { TopAppBar(title = { Text("Stocky") }) },
        ) { padding ->
            LazyColumn(contentPadding = padding) {
                items(pantryItems) { PantryRow(it) }
            }
        }
    }
}

Scaffold ya tiene en cuenta la barra superior e inferior que se le dan, así que la mayoría de las pantallas solo necesitan conectar ese único parámetro. La trampa es hacerlo dos veces: Scaffold aplica los insets de safe-drawing a su valor padding, y si un hijo también llama a .systemBarsPadding() encima de eso, obtienes el doble del espacio bajo la barra de estado — una franja en blanco que parece un error de diseño, pero en realidad es un error en el código de insets.

Dónde hay que manejar los insets a mano

No todas las pantallas pasan por Scaffold. Tres puntos que tuve que arreglar a mano en Granyn, Stocky y Mintly:

  • Hojas inferiores (bottom sheets) y diálogos. Un ModalBottomSheet dibuja su propia superficie fuera del padding del Scaffold padre, así que su contenido necesita su propio .navigationBarsPadding() en la columna más interna, o la última fila queda bajo la barra de gestos.
  • El teclado. WindowInsets.ime es un inset separado de navigationBars, y se anima mientras el teclado se abre y se cierra. Un campo de texto anclado en la parte inferior de una pantalla necesita .imePadding(), no .navigationBarsPadding() — ambos se superponen cuando el teclado está cerrado y divergen en el momento en que se abre.
  • Imágenes o cabeceras a sangre. Cuando quieres deliberadamente que el contenido se dibuje bajo la barra de estado — una galería de capturas, una imagen destacada — aplica los insets a los controles interactivos que están encima (botón de atrás, icono de compartir) con .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)) en lugar de ponerle padding a todo el contenedor, o perderás el efecto que buscabas.

El único hábito que atrapa la mayoría de esto antes de que lo haga un usuario: activa la navegación por gestos y la personalización “Demo mode” de la barra de estado en el emulador, y revisa cada pantalla con un notch simulado activado. La navegación de tres botones y una barra de estado sencilla ocultan casi todos los errores de insets que vas a lanzar.

Retroceso predictivo: de un corte brusco a una previsualización

El otro cambio es de comportamiento, no de diseño. El retroceso predictivo permite que el sistema muestre dónde aterrizará un gesto de retroceso — una previsualización que se encoge de la pantalla subyacente — antes de que el usuario levante el dedo, para que pueda ver el destino y abortar a mitad del gesto. Las apps que no se han sumado todavía siguen obteniendo un cambio de pantalla plano e instantáneo; el gesto funciona, pero se ve como un corte brusco junto a cualquier otra app compatible con retroceso predictivo en el dispositivo.

Sumarse requiere una bandera en el manifiesto más un callback de Compose:

<!-- AndroidManifest.xml -->
<application android:enableOnBackInvokedCallback="true">
PredictiveBackHandler(enabled = showDetail) { progress ->
    try {
        progress.collect { backEvent ->
            // backEvent.progress: 0f (inicio) → 1f (confirmado)
            scale = 1f - (backEvent.progress * 0.1f)
        }
        showDetail = false // gesto confirmado
    } catch (e: CancellationException) {
        scale = 1f // gesto abortado — vuelve al estado original
    }
}

PredictiveBackHandler (de androidx.activity.compose) te da un Flow de eventos de progreso en lugar de un único callback, que es la parte que confunde a quien viene de BackHandler. Lo recolectas durante todo el gesto, animas en función de backEvent.progress, y la CancellationException en un gesto abortado es la salida normal, no un error — así es como el sistema te dice que el usuario soltó antes de confirmar. Usé exactamente esto en la pantalla de detalle de sesión de Mintly: la vista del temporizador ahora se encoge ligeramente y revela la lista detrás mientras deslizas hacia atrás, y vuelve limpiamente a su tamaño completo si sueltas antes de tiempo, en lugar de que la vista simplemente desaparezca.

Qué revisar de verdad antes de publicar

  • Cada pantalla se renderiza correctamente con la navegación por gestos activada y la de tres botones desactivada — el modo en el que viene la mayoría de dispositivos nuevos.
  • No hay huecos duplicados bajo la barra de estado (la trampa de Scaffold + .systemBarsPadding() manual de arriba).
  • Las hojas inferiores y los snackbars quedan libres de la barra de gestos en un dispositivo sin botones de navegación físicos.
  • Los campos de texto cerca de la parte inferior de una pantalla usan .imePadding() y no saltan cuando se abre el teclado.
  • Al menos una pantalla con navegación real (no solo la primera que pruebas) tiene el retroceso predictivo conectado, para que el gesto no se sienta inconsistente entre pantallas.

Nada de esto es difícil una vez que te has topado con ello. Simplemente es invisible en un emulador estándar con navegación de tres botones — que es exactamente la configuración que la mayoría dejamos por defecto — y exactamente por eso se lanza roto más a menudo de lo que debería.