Navigation Drawer en Android con Jetpack Compose (Menú Lateral)

- Andrés Cruz - EN In english

Video thumbnail

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 rememberDrawerState que almacena si el Drawer se encuentra abierto (DrawerValue.Open) o cerrado (DrawerValue.Closed). Por defecto, siempre lo inicializamos en DrawerValue.Closed para 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 un CoroutineScope (obtenido mediante rememberCoroutineScope()) para lanzarlas de forma asíncrona con scope.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 nuestro Scaffold).

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 Text con estilos tipográficos prominentes como MaterialTheme.typography.titleLarge, complementado con un Spacer(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 estado selectedItem = index y cerramos el menú lanzando una corrutina con scope.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 con Modifier.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 navigationIcon colocamos un IconButton con el icono Icons.Default.Menu. Al pulsarlo, ejecutamos scope.launch { drawerState.open() } para abrir el menú lateral con su correspondiente animación.
  • Contenido principal: El Scaffold proporciona un objeto padding (de tipo PaddingValues) que debemos aplicar obligatoriamente con Modifier.padding(padding) al contenedor interno (como un Box o Column) para garantizar que la vista respete las barras del sistema y no quede oculta bajo la TopAppBar.
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 ViewModel o 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:

menú tradicional en android

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:

Menú lateral final

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 valor start es 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:

Menú lateral header

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

Menú lateral final segundo ejemplo

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:

Menú lateral final segundo ejemplo

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:

Menú lateral final segundo ejemplo

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 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" (o start):
menú lateral en android alineado a la izquierda
  • android:layout_gravity="right" (o end):
menú lateral en android alineado a la derecha

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, ModalDrawerSheet y NavigationDrawerItem. 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 DrawerLayout junto con NavigationView sigue 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.

Aprende a crear un Navigation Drawer (menú lateral) en Android con Jetpack Compose y Material 3 usando ModalNavigationDrawer, DrawerState, corrutinas y ModalDrawerSheet paso a paso.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

Acepto recibir anuncios de interes sobre este Blog.