Índice de contenido
- Implementación de un Navigation Drawer en Jetpack Compose (Material 3)
- ¿Por qué el Scaffold se implementa dentro del ModalNavigationDrawer?
- ️ Gestión de Estados y Corrutinas
- Estructura del ModalNavigationDrawer y ModalDrawerSheet
- El contenido del Drawer: ModalDrawerSheet y NavigationDrawerItem
- El Scaffold y la barra superior (TopAppBar)
- Menús estáticos vs. dinámicos y ejemplos completos en Jetpack Compose
- Navigation Drawer en XML y Vistas Clásicas (Enfoque Tradicional)
- Creando un menú lateral con NavigationView en XML
- Propiedades clave del NavigationView
- Definir una cabecera (Header Layout)
- Grupos y submenús en el menú lateral XML
- Comportamiento seleccionable (checkableBehavior) y submenús anidados
- Creación del NavigationDrawer con ListView (Enfoque Histórico Legacy)
- 1. Creando el Drawer Layout con ListView
- 2. Referenciando el Drawer Layout y el ListView en código
- 3. Definiendo el Adaptador (Adapter) con la lista de opciones
- 4. Configurando los eventos de apertura y cierre con el icono de la aplicación
- Conclusión: ¿Qué enfoque deberías utilizar?
El Navigation Drawer (o menú lateral deslizable) es uno de los componentes de navegación más importantes y reconocibles en el ecosistema Android y las guías de Material Design. Se trata de un panel interactivo que se expande y contrae desde el lateral de la pantalla (habitualmente desde el borde izquierdo o start), mostrando los destinos principales y opciones clave de navegación de la aplicación.
El usuario puede desplegar este menú lateral realizando un gesto táctil de arrastre (swipe) desde el extremo izquierdo hacia la derecha, o bien tocando el icono de navegación (el clásico botón de hamburguesa) situado en la barra de herramientas superior (TopAppBar).
En aplicaciones que cuentan con más de cinco destinos principales o que presentan estructuras jerárquicas con distintas categorías, el Navigation Drawer resulta una solución mucho más limpia y escalable que una barra de navegación inferior (NavigationBar), permitiendo organizar el contenido sin recargar la vista principal.
Si buscas una alternativa pensada para menús contextuales o acciones emergentes temporales, anteriormente vimos cómo funcionan los Bottom Sheets en Android Studio.
Implementación de un Navigation Drawer en Jetpack Compose (Material 3)
En Jetpack Compose, y concretamente bajo las directrices modernas de Material 3 (androidx.compose.material3), el menú lateral se implementa a través del composable ModalNavigationDrawer. A diferencia del sistema tradicional de vistas en XML (donde dependíamos de DrawerLayout y NavigationView), aquí todo el comportamiento y la interfaz se definen de manera 100% declarativa y reactiva.
Vamos a ver paso a paso cómo crear nuestro propio Navigation Drawer. Comenzaremos con un ejemplo básico para asimilar su mecánica de estados y corrutinas, y posteriormente veremos cómo enriquecerlo con iconos, insignias numéricas (badges), separadores y submenús.
La arquitectura de un Navigation Drawer es más elaborada que la de un botón o una tarjeta habitual: al igual que ocurre con los diálogos y los Bottom Sheets, requiere orquestar la visibilidad reactiva, las animaciones de deslizamiento y su acoplamiento con el Scaffold que sostiene la pantalla.
¿Por qué el Scaffold se implementa dentro del ModalNavigationDrawer?
En la estructura de Jetpack Compose, notarás que no es el Scaffold el que envuelve al Drawer, sino que el ModalNavigationDrawer actúa como contenedor raíz y envuelve al Scaffold dentro de su parámetro de contenido (content).
La razón técnica es que el ModalNavigationDrawer necesita controlar tanto el panel que se desliza como la capa de atenuación oscurecida de fondo (conocida como scrim) que cubre el resto de la interfaz cuando el menú está abierto. Al envolver al Scaffold, el menú lateral puede superponerse suavemente sobre la barra superior (TopAppBar) y todo el lienzo visual de la aplicación.
Para que este sistema funcione de forma reactiva, debemos declarar sus estados dentro del ciclo de vida composable haciendo uso de las funciones de memoria de Compose. Si intentaras instanciar estos estados de forma imperativa fuera del entorno composable (por ejemplo, directamente en el cuerpo plano de onCreate() en la Activity), Android arrojaría un error, ya que requieren estar ligados al árbol de recomposición:
// 1. Estado reactivo para abrir y cerrar el Navigation Drawer
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
// 2. Ámbito de corrutina para disparar las animaciones suspendidas
val scope = rememberCoroutineScope()
// 3. Estado para rastrear qué opción del menú está seleccionada
var selectedItem by remember { mutableIntStateOf(0) }️ Gestión de Estados y Corrutinas
Para controlar con precisión la visibilidad y el comportamiento del menú lateral, necesitamos apoyarnos en estos elementos esenciales:
- DrawerState: Es un objeto de estado gestionado mediante
rememberDrawerStateque almacena si el Drawer se encuentra abierto (DrawerValue.Open) o cerrado (DrawerValue.Closed). Por defecto, siempre lo inicializamos enDrawerValue.Closedpara que la pantalla principal cargue limpia. - CoroutineScope: Al igual que sucede al disparar un
Snackbar, abrir (drawerState.open()) u ocultar (drawerState.close()) el panel lateral conlleva una animación fluida con especificaciones de resorte y tiempo. Debido a que estas funciones son de suspensión (suspend functions), no pueden llamarse directamente desde un evento de clic síncrono; necesitamos unCoroutineScope(obtenido medianterememberCoroutineScope()) para lanzarlas de forma asíncrona conscope.launch { ... }sin bloquear el hilo de interfaz de usuario. - selectedItem: Variable de estado creada con
remember { mutableIntStateOf(0) }que registra el índice de la opción activa. Cuando el usuario pulsa un ítem, este valor se actualiza y desencadena la recomposición de los componentes dependientes.
Estructura del ModalNavigationDrawer y ModalDrawerSheet
El componente central es el composable ModalNavigationDrawer. Este componente recibe el drawerState que definimos anteriormente y nos pide implementar dos bloques o lambdas fundamentales:
drawerContent: Define la interfaz visual que aparecerá dentro del panel lateral cuando este se abra.content: Define la pantalla regular visible del usuario sobre la cual se desplegará el Drawer (donde colocaremos nuestroScaffold).
El contenido del Drawer: ModalDrawerSheet y NavigationDrawerItem
Dentro del parámetro drawerContent, utilizamos el contenedor ModalDrawerSheet de Material 3. Este componente se encarga de aplicar automáticamente las dimensiones recomendadas, el color tonal del contenedor, la elevación y las esquinas redondeadas en el borde exterior del menú.
Dentro de ModalDrawerSheet estructuramos los elementos que componen el menú lateral:
- Encabezado (Header): Puedes colocar un
Textcon estilos tipográficos prominentes comoMaterialTheme.typography.titleLarge, complementado con unSpacer(Modifier.height(12.dp))para dejar un margen superior adecuado frente a la barra de estado. - Divisores: El composable
HorizontalDivider()nos ayuda a separar visualmente la cabecera de las opciones o crear delimitaciones claras entre grupos de navegación. - Ítems de navegación: Para representar cada destino, iteramos sobre una lista de opciones utilizando el componente especializado
NavigationDrawerItem.
Cada NavigationDrawerItem cuenta con propiedades muy intuitivas:
label: La etiqueta de texto que describe la opción (ej.label = { Text("Inicio") }).selected: Booleano que indica si la opción actual es la que está seleccionada (selected = index == selectedItem).onClick: Callback que se ejecuta cuando el usuario pulsa el ítem. Aquí actualizamos el estadoselectedItem = indexy cerramos el menú lanzando una corrutina conscope.launch { drawerState.close() }.icon(opcional): Composable para renderizar un icono descriptivo junto al texto.badge(opcional): Insignia para mostrar contadores o etiquetas informativas (como notificaciones pendientes).modifier: Permite agregar el padding estándar conModifier.padding(NavigationDrawerItemDefaults.ItemPadding)para obtener la apariencia de píldora redondeada propia de Material 3.
Veamos cómo se ensambla el ModalNavigationDrawer con su ModalDrawerSheet:
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var selectedItem by remember { mutableIntStateOf(0) }
val items = listOf("Inicio", "Perfil", "Ajustes")
// Contenedor principal del Drawer
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
// Contenido visual del menú lateral
ModalDrawerSheet {
Spacer(Modifier.height(12.dp))
Text(
text = "Mi App",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleLarge
)
HorizontalDivider()
items.forEachIndexed { index, label ->
NavigationDrawerItem(
label = { Text(label) },
selected = index == selectedItem,
onClick = {
selectedItem = index
// Cerramos el drawer después de seleccionar
scope.launch { drawerState.close() }
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
) {
// Aquí ubicaremos el Scaffold con la pantalla principal
}El Scaffold y la barra superior (TopAppBar)
Una vez definido el panel lateral en drawerContent, colocamos el Scaffold en el cuerpo principal del ModalNavigationDrawer. El Scaffold nos ofrece la estructura base para disponer la barra superior y el área de contenido:
- TopAppBar: En la barra superior mostramos el título de la pantalla activa (el cual puede variar dinámicamente según la opción elegida mediante
items[selectedItem]) y el icono de navegación. - Evento de apertura: En el parámetro
navigationIconcolocamos unIconButtoncon el iconoIcons.Default.Menu. Al pulsarlo, ejecutamosscope.launch { drawerState.open() }para abrir el menú lateral con su correspondiente animación. - Contenido principal: El
Scaffoldproporciona un objetopadding(de tipoPaddingValues) que debemos aplicar obligatoriamente conModifier.padding(padding)al contenedor interno (como unBoxoColumn) para garantizar que la vista respete las barras del sistema y no quede oculta bajo laTopAppBar.
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
// ModalDrawerSheet con los ítems de navegación...
}
) {
// Contenido de la pantalla principal
Scaffold(
topBar = {
TopAppBar(
title = { Text("Pantalla de ${items[selectedItem]}") },
navigationIcon = {
IconButton(onClick = {
// Abrimos el drawer al pulsar el icono de menú hamburguesa
scope.launch { drawerState.open() }
}) {
Icon(Icons.Default.Menu, contentDescription = "Abrir menú de navegación")
}
}
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text(text = "Contenido de ${items[selectedItem]}")
}
}
}Menús estáticos vs. dinámicos y ejemplos completos en Jetpack Compose
Existen dos estrategias habituales a la hora de poblar las opciones de tu Navigation Drawer según la complejidad del proyecto:
- Implementación Fija o Estática: Las opciones se declaran directamente como una lista en el código (por ejemplo
listOf("Inicio", "Perfil", "Ajustes")). Es una técnica rápida y adecuada para pantallas fijas de configuración o menús que no varían entre sesiones. - Implementación Dinámica: Los elementos provienen de un modelo de datos administrado por un
ViewModelo una API REST. Esta aproximación permite mostrar badges numéricos con notificaciones en tiempo real, filtrar destinos según los permisos o el rol del usuario conectado y asociar iconos dinámicos a cada ítem.
Si necesitas organizar submenús temáticos dentro de tu menú lateral, basta con añadir títulos de sección con Text (utilizando estilos como MaterialTheme.typography.titleMedium) y delimitarlos con HorizontalDivider.
A continuación tienes el código completo de la MainActivity, incluyendo tanto el ejemplo básico funcional como un ejemplo avanzado (DetailedDrawerExample) con iconos, insignias de conteo (badge), submenús y soporte de desplazamiento vertical mediante Modifier.verticalScroll(rememberScrollState()):
package com.example.myproyectandroid
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.Help
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Button
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.NavigationDrawerItemDefaults
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.example.myproyectandroid.ui.theme.MyProyectAndroidTheme
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MyProyectAndroidTheme {
EjemploDrawerCompleto()
/*
// Alternativa con el Drawer detallado y contenido personalizado:
DetailedDrawerExample { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Text("¡Hola! Estoy dentro del cuerpo de la pantalla")
Button(onClick = { /* Acción */ }) {
Text("Botón de prueba")
}
}
}
*/
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EjemploDrawerCompleto() {
// 1. Estado para abrir/cerrar el Drawer
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
// 2. Estado para saber qué ítem está seleccionado
var selectedItem by remember { mutableIntStateOf(0) }
val items = listOf("Inicio", "Perfil", "Ajustes")
// 3. El contenedor principal del Drawer
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
// Contenido visual del menú lateral
ModalDrawerSheet {
Spacer(Modifier.height(12.dp))
Text(
text = "Mi App",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleLarge
)
HorizontalDivider()
items.forEachIndexed { index, label ->
NavigationDrawerItem(
label = { Text(label) },
selected = index == selectedItem,
onClick = {
selectedItem = index
// Cerramos el drawer después de hacer clic
scope.launch { drawerState.close() }
},
modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
)
}
}
}
) {
// 4. Contenido de la pantalla principal con Scaffold
Scaffold(
topBar = {
TopAppBar(
title = { Text("Pantalla de ${items[selectedItem]}") },
navigationIcon = {
IconButton(onClick = {
// Abrimos el drawer al pulsar el icono de menú hamburguesa
scope.launch { drawerState.open() }
}) {
Icon(Icons.Default.Menu, contentDescription = "Abrir menú")
}
}
)
}
) { padding ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding),
contentAlignment = Alignment.Center
) {
Text(text = "Contenido de ${items[selectedItem]}")
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DetailedDrawerExample(
content: @Composable (PaddingValues) -> Unit
) {
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet {
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.verticalScroll(rememberScrollState())
) {
Spacer(Modifier.height(12.dp))
Text(
text = "Panel de Navegación",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleLarge
)
HorizontalDivider()
// Sección 1 de opciones
Text(
text = "Principal",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleMedium
)
NavigationDrawerItem(
label = { Text("Inicio") },
selected = false,
onClick = { /* Manejar navegación a Inicio */ }
)
NavigationDrawerItem(
label = { Text("Favoritos") },
selected = false,
onClick = { /* Manejar navegación a Favoritos */ }
)
HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))
// Sección 2 de opciones (Submenú con iconos e insignias)
Text(
text = "Preferencias",
modifier = Modifier.padding(16.dp),
style = MaterialTheme.typography.titleMedium
)
NavigationDrawerItem(
label = { Text("Configuración") },
selected = false,
icon = { Icon(Icons.Outlined.Settings, contentDescription = null) },
badge = { Text("20") }, // Indicador numérico de badge
onClick = { /* Manejar clic en configuración */ }
)
NavigationDrawerItem(
label = { Text("Ayuda y soporte") },
selected = false,
icon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) },
onClick = { /* Manejar clic en ayuda */ }
)
Spacer(Modifier.height(12.dp))
}
}
}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("Navigation Drawer Avanzado") },
navigationIcon = {
IconButton(onClick = {
scope.launch {
if (drawerState.isClosed) {
drawerState.open()
} else {
drawerState.close()
}
}
}) {
Icon(Icons.Default.Menu, contentDescription = "Menú")
}
}
)
}
) { innerPadding ->
content(innerPadding)
}
}
}En Jetpack Compose la interfaz es una representación directa del estado. Gracias a este paradigma reactivo, la vista responde fluidamente a los gestos táctiles de arrastre mientras que las transiciones programáticas se ejecutan de manera limpia a través de corrutinas.
Navigation Drawer en XML y Vistas Clásicas (Enfoque Tradicional)
Aunque Jetpack Compose es el presente y futuro del desarrollo en Android, muchas aplicaciones existentes continúan basadas en el sistema tradicional de vistas con archivos XML. Conocer cómo se estructura un menú lateral mediante DrawerLayout y NavigationView es indispensable si trabajas en el mantenimiento o modernización gradual de un proyecto existente.
El menú lateral mediante DrawerLayout representó en su momento un salto de calidad frente al menú de opciones clásico de la barra de acciones superior:

Creando un menú lateral con NavigationView en XML
Con la llegada de la librería de Material Design (hoy en AndroidX), Google estandarizó la creación del menú lateral mediante el componente NavigationView, dejando atrás la necesidad de armar adaptadores manuales con ListView:

En este apartado veremos cómo configurar un Navigation Drawer moderno en XML combinando DrawerLayout y NavigationView. Si necesitas consultar la implementación histórica original basada en ListView, puedes saltar directamente a la sección de ListView legacy.
En el diseño XML de tu actividad (por ejemplo activity_main.xml), definimos la jerarquía básica con AndroidX:
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!-- Contenido principal de la actividad (FrameLayout, CoordinatorLayout, etc.) -->
<!-- Menú lateral con NavigationView -->
<com.google.android.material.navigation.NavigationView
android:id="@+id/navView"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
app:menu="@menu/menu"
app:headerLayout="@layout/nav_header"
android:fitsSystemWindows="true" />
</androidx.drawerlayout.widget.DrawerLayout>Como puedes observar, la estructura consta de dos piezas fundamentales:
- DrawerLayout: Es el contenedor raíz de nivel superior. Aloja tanto el contenido de la pantalla principal (botones, textos, fragmentos) como el panel deslizante.
- NavigationView: Representa el panel visual del menú lateral. Se encarga de renderizar la cabecera y la lista de opciones a partir de recursos XML declarativos.
Propiedades clave del NavigationView
Si analizamos el código anterior, destacan varios atributos indispensables:
android:layout_gravity="start": Define desde qué lateral se deslizará el menú. El valorstartes la buena práctica recomendada porque respeta la dirección de lectura del sistema (izquierda para idiomas LTR y derecha para idiomas RTL).android:fitsSystemWindows="true": Permite que el menú lateral se dibuje de forma armónica detrás de las barras del sistema (como la barra de estado o *status bar*).app:menu="@menu/menu": Vincula el archivo de recursos de menú que define los ítems de navegación.app:headerLayout="@layout/nav_header": Especifica un layout personalizado para la cabecera del menú.
El archivo de menú (res/menu/menu.xml) define los títulos y los iconos correspondientes a cada opción:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/op1"
android:title="Opción 1"
android:icon="@drawable/ic_android" />
<item
android:id="@+id/op2"
android:title="Opción 2"
android:icon="@drawable/ic_android" />
<item
android:id="@+id/op3"
android:title="Opción 3"
android:icon="@drawable/ic_attach_file" />
</menu>En este menú, cada etiqueta <item> cuenta con su identificador único (android:id), el texto descriptivo (android:title) y un recurso gráfico asignado como icono (android:icon).
Definir una cabecera (Header Layout)
Mediante la propiedad app:headerLayout podemos referenciar un layout independiente (por ejemplo res/layout/nav_header.xml) para diseñar la cabecera del Drawer. Habitualmente se incluye el avatar del usuario, su nombre y su correo electrónico:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical"
android:background="@drawable/nav_drawer_header"
android:padding="16dp">
<ImageView
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center"
android:layout_marginTop="15dp"
android:layout_marginBottom="10dp"
android:src="@mipmap/ic_launcher" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#FFF"
android:gravity="center"
android:text="Nombre del Usuario" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#FFF"
android:gravity="center"
android:text="usuario@correo.com" />
</LinearLayout>El resultado visual del encabezado luce de la siguiente forma:

Al integrar el NavigationView con su cabecera y archivo de menú, obtenemos un panel lateral completamente operativo:

Grupos y submenús en el menú lateral XML
Las agrupaciones mediante la etiqueta <group> resultan muy útiles cuando deseamos organizar opciones por temática o categoría. Además de introducir una línea divisoria automática, permiten activar o desactivar secciones enteras de forma programática desde código Java o Kotlin:
<group android:id="@+id/group1">
<item
android:id="@+id/op3_1"
android:title="Opción 3_1"
android:icon="@drawable/ic_attach_file" />
<item
android:id="@+id/op3_2"
android:title="Opción 3_2"
android:icon="@drawable/ic_attach_file" />
</group>Obteniendo la siguiente presentación estructurada:

Comportamiento seleccionable (checkableBehavior) y submenús anidados
Podemos exigir que solo un elemento permanezca seleccionado a la vez dentro de un grupo aplicando la propiedad android:checkableBehavior="single" (comportamiento tipo radio button). Asimismo, es posible anidar etiquetas <menu> dentro de un <item> para crear submenús con subtítulo propio:
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/op1"
android:title="Opción 1"
android:icon="@drawable/ic_android" />
<item
android:id="@+id/op2"
android:title="Opción 2"
android:icon="@drawable/ic_android" />
<item
android:id="@+id/op3"
android:title="Opción 3"
android:icon="@drawable/ic_attach_file" />
<group android:id="@+id/group1">
<item
android:id="@+id/op3_1"
android:title="Opción 3_1"
android:icon="@drawable/ic_attach_file" />
<item
android:id="@+id/op3_2"
android:title="Opción 3_2"
android:icon="@drawable/ic_attach_file" />
</group>
<group
android:id="@+id/group2"
android:checkableBehavior="single">
<item
android:id="@+id/navigation_subheader"
android:title="Otro Grupo">
<menu>
<item
android:id="@+id/menu_opcion_1"
android:icon="@drawable/ic_attach_file"
android:title="Op 4.1" />
<item
android:id="@+id/menu_opcion_2"
android:icon="@drawable/ic_android"
android:title="Op 4.2" />
</menu>
</item>
</group>
</menu>Con esta configuración logramos submenús segmentados y con títulos de cabecera independientes:

Creación del NavigationDrawer con ListView (Enfoque Histórico Legacy)
Esta sección documenta la implementación histórica de un menú lateral mediante ListView y adaptadores manuales. Para proyectos nuevos que utilicen vistas tradicionales XML, se recomienda NavigationView o RecyclerView; sin embargo, este método continúa siendo de gran valor educativo para entender la mecánica interna del DrawerLayout:
- 1. Creando un Drawer Layout con ListView
- 2. Referenciando el Drawer Layout y el ListView en código
- 3. Definiendo el Adaptador (Adapter) con la lista de opciones
- 4. Configurando los eventos de apertura y cierre con el icono de la aplicación
1. Creando el Drawer Layout con ListView
En este enfoque, añadimos el DrawerLayout como elemento raíz de la actividad, ubicando en su interior la vista de contenido principal y un ListView que actuará como panel deslizable del menú lateral:
<androidx.drawerlayout.widget.DrawerLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- Contenido principal de la actividad -->
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hola Mundo!" />
</RelativeLayout>
<!-- Menú lateral mediante ListView -->
<ListView
android:id="@+id/list_view"
android:layout_width="300dp"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="#007095"
android:choiceMode="singleChoice"
android:divider="#005875"
android:dividerHeight="2dp" />
</androidx.drawerlayout.widget.DrawerLayout>El contenido regular de la actividad debe situarse como primer elemento hijo dentro de androidx.drawerlayout.widget.DrawerLayout.
Podemos especificar la orientación desde donde se desliza el menú modificando el atributo android:layout_gravity:
android:layout_gravity="left"(ostart):

android:layout_gravity="right"(oend):

Por directriz y convención de Material Design, la ubicación estándar para el menú principal es el lado izquierdo (start o left). No obstante, situarlo a la derecha (end o right) resulta muy útil si necesitas implementar un panel secundario de filtros o acciones complementarias.
2. Referenciando el Drawer Layout y el ListView en código
Una vez definido el XML, en el método onCreate() de nuestra actividad obtenemos las referencias al ListView y al DrawerLayout:
ListView listView = (ListView) findViewById(R.id.list_view);
DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);3. Definiendo el Adaptador (Adapter) con la lista de opciones
Con las vistas referenciadas, creamos el arreglo de destinos y lo enlazamos al ListView mediante un Adapter como ArrayAdapter:
String[] opciones = { "Opción 1", "Opción 2", "Opción 3", "Opción 4" };
listView.setAdapter(new ArrayAdapter<String>(
this,
android.R.layout.simple_list_item_1,
android.R.id.text1,
opciones
));4. Configurando los eventos de apertura y cierre con el icono de la aplicación
Para permitir que el usuario abra y cierre el menú pulsando el icono de la barra de acciones superior, debemos habilitar el botón de navegación e interceptar la selección del elemento android.R.id.home dentro de onOptionsItemSelected():
// Habilitar el botón de navegación en la barra de acciones:
// getActionBar().setDisplayHomeAsUpEnabled(true);
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// Comprobar si el menú lateral está abierto o cerrado
if (drawerLayout.isDrawerOpen(listView)) {
drawerLayout.closeDrawers();
} else {
drawerLayout.openDrawer(listView);
}
return true;
}
return super.onOptionsItemSelected(item);
}Conclusión: ¿Qué enfoque deberías utilizar?
La evolución del Navigation Drawer en Android refleja el avance de las herramientas de desarrollo en la plataforma:
- Para nuevos proyectos: Utiliza siempre Jetpack Compose con
ModalNavigationDrawer,ModalDrawerSheetyNavigationDrawerItem. Su enfoque declarativo reduce drásticamente las líneas de código, elimina la necesidad de archivos XML auxiliares y gestiona automáticamente los estados visuales y temas de Material 3. - Para proyectos existentes en XML: El combo
DrawerLayoutjunto conNavigationViewsigue siendo la solución estándar y robusta para mantener arquitecturas tradicionales basadas en vistas.
Siguiente paso: aprende a crear tu sistema de Tabs en Jetpack Compose con HorizontalPager.