El patrón de navegación adaptativa
El cambio más visible al adaptar una app para pantallas grandes es la navegación. La regla de Material3 es clara: la navegación va donde hay espacio, y en el eje donde no compite con el contenido.
# Compact (< 600dp de ancho) — teléfono portrait:
# → BottomNavigationBar: navegación en la parte inferior
# → Máximo 5 destinos, solo íconos o ícono + etiqueta corta
# Medium (600-840dp) — tablet portrait, teléfono landscape:
# → NavigationRail: barra lateral estrecha izquierda
# → Ícono + etiqueta, puede tener FAB encima
# Expanded (> 840dp) — tablet landscape, ChromeOS ventana grande:
# → NavigationDrawer permanente: panel lateral fijo con texto completo
# → El contenido se desplaza a la derecha del drawer
BottomNavigationBar — Compact
@Composable
fun NavegacionCompact(
destinoActual: Destino,
onNavegar: (Destino) -> Unit
) {
NavigationBar {
destinos.forEach { destino ->
NavigationBarItem(
selected = destinoActual == destino,
onClick = { onNavegar(destino) },
icon = { Icon(destino.icono, contentDescription = destino.label) },
label = { Text(destino.label) }
)
}
}
}
NavigationRail — Medium
@Composable
fun NavegacionMedium(
destinoActual: Destino,
onNavegar: (Destino) -> Unit,
contenido: @Composable () -> Unit
) {
Row(modifier = Modifier.fillMaxSize()) {
NavigationRail(
header = {
// FAB opcional en la parte superior del rail
FloatingActionButton(onClick = { /* acción principal */ }) {
Icon(Icons.Default.Add, "Nuevo")
}
}
) {
// Spacer para empujar los items al centro verticalmente
Spacer(modifier = Modifier.weight(1f))
destinos.forEach { destino ->
NavigationRailItem(
selected = destinoActual == destino,
onClick = { onNavegar(destino) },
icon = { Icon(destino.icono, contentDescription = destino.label) },
label = { Text(destino.label) }
)
}
Spacer(modifier = Modifier.weight(1f))
}
// El contenido ocupa el espacio restante
Box(modifier = Modifier.weight(1f)) {
contenido()
}
}
}
NavigationDrawer permanente — Expanded
@Composable
fun NavegacionExpanded(
destinoActual: Destino,
onNavegar: (Destino) -> Unit,
contenido: @Composable () -> Unit
) {
PermanentNavigationDrawer(
drawerContent = {
PermanentDrawerSheet(modifier = Modifier.width(240.dp)) {
// Header del drawer — logo o nombre de la app
Text(
"Mi App",
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp)
)
Spacer(modifier = Modifier.height(8.dp))
destinos.forEach { destino ->
NavigationDrawerItem(
icon = { Icon(destino.icono, contentDescription = null) },
label = { Text(destino.label) },
selected = destinoActual == destino,
onClick = { onNavegar(destino) },
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
) {
contenido()
}
}
AdaptiveNavigationSuite — el todo en uno
Material3 incluye NavigationSuiteScaffold que elige automáticamente el tipo de navegación según el WindowSizeClass. En la mayoría de los casos no necesitás el código manual de arriba:
// implementation("androidx.compose.material3.adaptive:adaptive-navigation-suite:1.0.0")
@Composable
fun AppConNavegacionAdaptativa() {
var destinoActual by remember { mutableStateOf(Destino.INICIO) }
NavigationSuiteScaffold(
navigationSuiteItems = {
destinos.forEach { destino ->
item(
icon = { Icon(destino.icono, contentDescription = destino.label) },
label = { Text(destino.label) },
selected = destinoActual == destino,
onClick = { destinoActual = destino }
)
}
}
) {
// Contenido de la pantalla activa
when (destinoActual) {
Destino.INICIO -> PantallaInicio()
Destino.BUSCAR -> PantallaBuscar()
Destino.PERFIL -> PantallaPerfil()
}
}
}
// NavigationSuiteScaffold elige automáticamente:
// → NavigationBar en Compact
// → NavigationRail en Medium
// → NavigationDrawer en Expanded
// Sin ningún when() manual
Adaptar el contenido — más allá de la navegación
// Adaptar la cuadrícula según el ancho disponible:
@Composable
fun GridAdaptativa(productos: List<Producto>) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val columnas = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> 2
WindowWidthSizeClass.MEDIUM -> 3
else -> 4
}
LazyVerticalGrid(
columns = GridCells.Fixed(columnas),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
items(productos, key = { it.id }) { producto ->
ProductoCard(producto)
}
}
}
// Adaptar el padding y márgenes:
@Composable
fun ContenidoConMargenesAdaptativos(content: @Composable () -> Unit) {
val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass
val padding = when (windowSizeClass.windowWidthSizeClass) {
WindowWidthSizeClass.COMPACT -> PaddingValues(horizontal = 16.dp)
WindowWidthSizeClass.MEDIUM -> PaddingValues(horizontal = 24.dp)
else -> PaddingValues(horizontal = 32.dp)
}
Box(modifier = Modifier.padding(padding)) { content() }
}
Orientación y cambios de configuración
// AndroidManifest.xml — permitir todas las orientaciones en tablets:
// Por default, muchas apps fuerzan portrait
// <activity
// android:screenOrientation="unspecified" ← dejar que el sistema decida
// android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout">
// En Compose: el estado sobrevive a rotaciones si está en el ViewModel
// No hacer nada especial — Compose recompone con el nuevo WindowSizeClass automáticamente
// Lo que SÍ hay que verificar manualmente:
// → Que no haya texto cortado en landscape
// → Que los diálogos no sean más grandes que la pantalla en portrait
// → Que los BottomSheets no bloqueen contenido importante en landscape