Prerrequisitos
- El proyecto DEBE usar Android Jetpack Compose.
- El proyecto DEBE apuntar a SDK 35 o superior. Si el SDK es inferior a 35, aumentar el SDK a 35.
Paso 1: planificar
- Localizar y analizar todas las clases Activity para detectar cuáles tienen soporte edge-to-edge existente. Para cada Activity que no lo tenga, planificar hacerla edge-to-edge.
- En cada Activity, localizar y analizar todos los componentes de listas y FAB para detectar cuáles tienen soporte edge-to-edge existente. Para cada componente sin soporte, planificar hacerlo edge-to-edge.
- En cada Activity, buscar
TextField,OutlinedTextFieldoBasicTextField. Si se encuentran, DEBES verificar que el IME no oculte el campo de entrada siguiendo la sección de IME de esta skill.
Paso 2: añadir soporte edge-to-edge
- Agregar
enableEdgeToEdgeantes desetContentenonCreateen cada Activity que aún no llame aenableEdgeToEdge. - Agregar
android:windowSoftInputMode="adjustResize"en el AndroidManifest.xml para todas las Activities que usen teclado suave.
Paso 3: aplicar insets
-
La app DEBE aplicar los insets del sistema, o alinear el contenido a las reglas, para que la UI crítica siga siendo tachable. Elige solo un método para evitar el doble padding:
- PREFERIDO: Cuando esté disponible, usa
Scaffolds y pasaPaddingValuesal lambda de contenido.
Scaffold { innerPadding -> // innerPadding tiene en cuenta las barras del sistema y cualquier componente del Scaffold LazyColumn( modifier = Modifier .fillMaxSize() .consumeWindowInsets(innerPadding), contentPadding = innerPadding ) { /* Contenido */ } }-
PREFERIDO: Cuando esté disponible, usa el manejo automático de insets o los modificadores de padding en componentes de Material.
- Los componentes de Material 3 gestionan las áreas seguras para sus propios componentes, incluyendo:
TopAppBarSmallTopAppBarCenterAlignedTopAppBarMediumTopAppBarLargeTopAppBarBottomAppBarModalDrawerSheetDismissibleDrawerSheetPermanentDrawerSheetModalBottomSheetNavigationBarNavigationRail
- Para componentes de Material 2, usa el parámetro
windowInsetspara aplicar insets manualmente enBottomAppBar,TopAppBaryBottomNavigation. NO apliques padding al contenedor padre; en su lugar, pasa los insets directamente al componente de la barra. Aplicar padding al contenedor padre evita que el fondo de la barra se dibuje en el área de la barra del sistema. Por ejemplo, paraTopAppBar, elige solo una de las siguientes opciones:- PREFERIDO:
TopAppBar(windowInsets = AppBarDefaults.topAppBarWindowInsets) TopAppBar(windowInsets = WindowInsets.systemBars.exclude(WindowInsets.navigationBars))TopAppBar(windowInsets = WindowInsets.systemBars.add(WindowInsets.captionBar))
- PREFERIDO:
- Los componentes de Material 3 gestionan las áreas seguras para sus propios componentes, incluyendo:
-
Para componentes fuera de un Scaffold, usa modificadores de padding, como
Modifier.safeDrawingPadding()oModifier.windowInsetsPadding(WindowInsets.safeDrawing).Box( modifier = Modifier .fillMaxSize() .safeDrawingPadding() ) { Button( onClick = {}, modifier = Modifier.align(Alignment.BottomCenter) ) { Text("Login") } } -
Para componentes profundamente anidados con padding excesivo, usa
WindowInsetsRulers(ej.Modifier.fitInside(WindowInsetsRulers.SafeDrawing.current)). Consulta la sección IME para un ejemplo de código. -
Cuando necesites que un elemento (ej. un encabezado personalizado o un scrim decorativo) tenga las mismas dimensiones que una barra del sistema, usa modificadores de tamaño de insets (ej.
Modifier.windowInsetsTopHeight(WindowInsets.systemBars)). Consulta la sección Listas para un ejemplo de código.
- PREFERIDO: Cuando esté disponible, usa
Scaffolds adaptativos
NavigationSuiteScaffoldgestiona las áreas seguras para sus propios componentes, comoNavigationRailoNavigationBar. Sin embargo, los scaffolds adaptativos (ej.NavigationSuiteScaffold,ListDetailPaneScaffold) no propagan PaddingValues a su contenido interno. DEBES aplicar insets a cada pantalla o componente individual (ej.contentPaddingde lista o padding de FAB) como se describe en Paso 3. NO apliquessafeDrawingPaddingo modificadores similares al padre deNavigationSuiteScaffold. Esto recorta e impide una pantalla edge-to-edge.
IME
- Para cada Activity con teclado suave, verifica que
android:windowSoftInputMode="adjustResize"esté configurado en el AndroidManifest.xml. NO usesSOFT_INPUT_ADJUST_RESIZEporque está obsoleto. Luego, mantén el foco en el campo de entrada. Elige una opción:-
- PREFERIDO: Agrega
Modifier.fitInside(WindowInsetsRulers.Ime.current)al contenedor de contenido. Esto es preferible aimePadding()porque reduce el jank y el padding extra causado por olvidar consumir insets aguas arriba en la jerarquía.
- PREFERIDO: Agrega
-
- Agrega
imePaddingal contenedor de contenido. El modificador de padding DEBE colocarse antes deModifier.verticalScroll(). NO usesModifier.imePadding()si el padre ya tiene en cuenta el IME concontentWindowInsets(ej.contentWindowInsets = WindowInsets.safeDrawing). Hacerlo causará doble padding.
- Agrega
-
Patrones de código para IMEs con Scaffolds
CORRECTO
CORRECTO porque contentWindowInsets contiene insets del IME, que se pasan al lambda de contenido como innerPadding.
// CORRECTO
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.verticalScroll(rememberScrollState())
) { /* Contenido */ }
}
CORRECTO porque fitInside ajusta el contenido a los insets del IME independientemente de contentWindowInsets.
// CORRECTO
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.fitInside(WindowInsetsRulers.Ime.current)
.verticalScroll(rememberScrollState())
) { /* Contenido */ }
}
CORRECTO porque el contentWindowInsets por defecto no contiene insets del IME, y imePadding() aplica los insets del IME:
// CORRECTO
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.consumeWindowInsets(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* Contenido */ }
}
INCORRECTO
INCORRECTO porque habrá padding excesivo cuando se abra el IME. Los insets del IME se aplican dos veces, una con innerPadding, que contiene los insets del IME de los valores de contentWindowInsets pasados, y otra con imePadding:
// INCORRECTO
Scaffold(contentWindowInsets = WindowInsets.safeDrawing) { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.imePadding()
.verticalScroll(rememberScrollState())
) { /* Contenido */ }
}
INCORRECTO porque el IME cubrirá el contenido. El contentWindowInsets por defecto del Scaffold NO contiene insets del IME.
// INCORRECTO
Scaffold() { innerPadding ->
Column(
modifier = Modifier
.padding(innerPadding)
.verticalScroll(rememberScrollState())
) { /* Contenido */ }
}
Patrones de código para IMEs sin Scaffolds
CORRECTO
Los siguientes ejemplos de código NO causarán padding excesivo.
// CORRECTO
Box(
// Insets consumidos
modifier = Modifier.safeDrawingPadding() // o imePadding(), safeContentPadding(), safeGesturesPadding()
) {
Column(
modifier = Modifier.imePadding()
) { /* Contenido */ }
}
// CORRECTO
Box(
// Insets consumidos
modifier = Modifier.windowInsetsPadding(WindowInsets.safeDrawing) // o WindowInsets.ime, WindowInsets.safeContent, WindowInsets.safeGestures
) {
Column(
modifier = Modifier.imePadding()
) { /* Contenido */ }
}
// CORRECTO
Box(
// Insets no consumidos, pero irrelevante debido a fitInside
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // o WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier
.fillMaxSize()
.fitInside(WindowInsetsRulers.Ime.current)
) { /* Contenido */ }
}
INCORRECTO
El siguiente ejemplo de código CAUSARÁ padding excesivo porque los insets del IME se aplican dos veces:
// INCORRECTO
Box(
// Insets no consumidos
modifier = Modifier.padding(WindowInsets.safeDrawing.asPaddingValues()) // o WindowInsets.ime.asPaddingValues(), WindowInsets.safeContent.asPaddingValues(), WindowInsets.safeGestures.asPaddingValues()
) {
Column(
modifier = Modifier.imePadding()
) { /* Contenido */ }
}
Contraste de la barra de navegación e íconos de las barras del sistema
-
Si la Activity usa
enableEdgeToEdgedeWindowCompat, DEBES establecerisAppearanceLightNavigationBarseisAppearanceLightStatusBarsal inverso del tema del dispositivo para apps que soporten tema claro y oscuro, para que los íconos de las barras del sistema sean legibles. Se recomienda hacer esto en tu archivo de tema. NO hagas esto si las Activities usanenableEdgeToEdgedeComponentActivity, porque maneja los colores de los íconos automáticamente.// Usar solo si se llama a `enableEdgeToEdge` desde `WindowCompat`. // Aplicar a tu archivo de tema. @Composable fun MyTheme( darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit ) { val view = LocalView.current if (!view.isInEditMode) { SideEffect { val window = (view.context as? Activity)?.window ?: return@SideEffect val controller = WindowCompat.getInsetsController(window, view) // Íconos oscuros para modo claro (!darkTheme), íconos claros para modo oscuro controller.isAppearanceLightStatusBars = !darkTheme controller.isAppearanceLightNavigationBars = !darkTheme } } MaterialTheme(content = content) } -
Si alguna pantalla usa un
Scaffoldo unNavigationSuiteScaffoldcon una barra inferior (ej.BottomAppBar,NavigationBar), establecewindow.isNavigationBarContrastEnforced = falseen la Activity correspondiente para SDK 29+. Esto evita que el sistema agregue un fondo translúcido a la barra de navegación, asegurando que los colores de tu barra inferior se extiendan hasta la parte inferior de la pantalla.
Listas
- Aplica el padding de insets (como el
innerPaddingdelScaffold) al parámetrocontentPaddingde los componentes desplazables (ej.LazyColumn,LazyRow). NO lo apliques comoModifier.padding()al contenedor padre de la lista, ya que esto recorta el contenido y evita que se desplace detrás de las barras del sistema. - Crea un composable translúcido que cubra la barra del sistema para que los íconos sigan siendo legibles.
class SystemBarProtectionSnippets : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// enableEdgeToEdge establece window.isNavigationBarContrastEnforced = true
// que se usa para agregar un scrim translúcido en la navegación de tres botones
enableEdgeToEdge()
setContent {
MyTheme {
// Contenido principal
MyContent()
// Después de dibujar el contenido principal, dibujar protección de la barra de estado
StatusBarProtection()
}
}
}
}
@Composable
private fun StatusBarProtection(
color: Color = MaterialTheme.colorScheme.surfaceContainer,
) {
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(
with(LocalDensity.current) {
(WindowInsets.statusBars.getTop(this) * 1.2f).toDp()
}
)
.background(
brush = Brush.verticalGradient(
colors = listOf(
color.copy(alpha = 1f),
color.copy(alpha = 0.8f),
Color.Transparent
)
)
)
)
}
Diálogos
Si ambas condiciones siguientes son verdaderas, entonces el diálogo es a pantalla completa y debe hacerse edge-to-edge:
DialogPropertiescontieneusePlatformDefaultWidth = false.- El diálogo llama a
Modifier.fillMaxSize().
Para hacer que un diálogo a pantalla completa sea edge-to-edge, establece decorFitsSystemWindows = false en DialogProperties.
Dialog(
onDismissRequest = { /* Manejar cierre */ },
properties = DialogProperties(
// 1. Permite que el diálogo abarque todo el ancho de la pantalla
usePlatformDefaultWidth = false,
// 2. Permite que el diálogo se dibuje detrás de las barras de estado y navegación
decorFitsSystemWindows = false
)
) { /* Contenido */ }
Lista de verificación
- ¿Cada
Activityllama aenableEdgeToEdge()? - ¿Está configurado
adjustResizeen elAndroidManifest.xml? - ¿Cada
TextField,OutlinedTextFieldoBasicTextFieldtiene un padre conimePadding(),fitInside,Modifier.safeDrawingPadding(),Modifier.safeContentPadding(),Modifier.safeGesturesPadding(), ocontentWindowInsetsestablecido aWindowInsets.safeDrawingoWindowInsets.ime? - ¿El primer y último elemento de la lista se dibujan alejados de las barras del sistema pasando insets a
contentPadding? - ¿Los FABs se dibujan sobre las barras de navegación ya sea estando dentro de un Scaffold o aplicando
Modifier.safeDrawingPadding()? - ¿El proyecto compila? Ejecuta
./gradlew buildpara asegurarte.