Navigation Drawer on Android with Jetpack Compose (Side Menu)

- Andrés Cruz - ES En español

Video thumbnail

The Navigation Drawer (or sliding side menu) is one of the most important and recognizable navigation components in the Android ecosystem and Material Design guidelines. It is an interactive panel that expands and collapses from the side of the screen (usually from the left edge or start), displaying the application's main destinations and key navigation options.

The user can open this side menu by performing a touch swipe gesture (swipe) from the far left to the right, or by tapping the navigation icon (the classic hamburger button) located in the top toolbar (TopAppBar).

In applications that have more than five main destinations or present hierarchical structures with different categories, the Navigation Drawer provides a much cleaner and more scalable solution than a bottom navigation bar (NavigationBar), allowing content to be organized without cluttering the main view.

If you are looking for an alternative designed for contextual menus or temporary pop-up actions, we previously covered how Bottom Sheets work in Android Studio.

Implementing a Navigation Drawer in Jetpack Compose (Material 3)

In Jetpack Compose, specifically under modern Material 3 guidelines (androidx.compose.material3), the side menu is implemented through the ModalNavigationDrawer composable. Unlike the traditional XML view system (where we relied on DrawerLayout and NavigationView), here all behavior and UI are defined in a 100% declarative and reactive manner.

Let's go step-by-step through creating our own Navigation Drawer. We will start with a basic example to grasp its state mechanics and coroutines, and later see how to enrich it with icons, numeric badges (badges), dividers, and submenus.

The architecture of a Navigation Drawer is more elaborate than that of a standard button or card: as with dialogs and Bottom Sheets, it requires orchestrating reactive visibility, sliding animations, and coupling with the Scaffold that holds the screen.

Why is the Scaffold implemented inside the ModalNavigationDrawer?

In the Jetpack Compose structure, you will notice that it is not the Scaffold wrapping the Drawer, but rather the ModalNavigationDrawer acting as the root container and wrapping the Scaffold inside its content parameter (content).

The technical reason is that the ModalNavigationDrawer needs to control both the sliding panel and the darkened background dimming layer (known as scrim) that covers the rest of the UI when the menu is open. By wrapping the Scaffold, the side menu can smoothly overlap the top bar (TopAppBar) and the entire visual canvas of the app.

For this system to work reactively, we must declare its states within the composable lifecycle using Compose memory functions. If you were to try to instantiate these states imperatively outside the composable scope (for instance, directly in the flat body of onCreate() in the Activity), Android would throw an error, as they require being tied to the recomposition tree:

// 1. Reactive state to open and close the Navigation Drawer
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)

// 2. Coroutine scope to trigger suspend animations
val scope = rememberCoroutineScope()

// 3. State to track which menu item is selected
var selectedItem by remember { mutableIntStateOf(0) }

️ State and Coroutine Management

To precisely control the visibility and behavior of the side menu, we need to rely on these essential elements:

  • DrawerState: A state object managed using rememberDrawerState that stores whether the Drawer is open (DrawerValue.Open) or closed (DrawerValue.Closed). By default, we always initialize it to DrawerValue.Closed so that the main screen loads cleanly.
  • CoroutineScope: Just as when triggering a Snackbar, opening (drawerState.open()) or hiding (drawerState.close()) the side panel involves a smooth animation with spring and timing specifications. Because these functions are suspend functions (suspend functions), they cannot be called directly from a synchronous click event; we need a CoroutineScope (obtained via rememberCoroutineScope()) to launch them asynchronously with scope.launch { ... } without blocking the UI thread.
  • selectedItem: State variable created with remember { mutableIntStateOf(0) } that records the index of the active option. When the user taps an item, this value updates and triggers the recomposition of dependent components.

Structure of ModalNavigationDrawer and ModalDrawerSheet

The central component is the ModalNavigationDrawer composable. This component receives the drawerState defined earlier and requires us to implement two fundamental blocks or lambdas:

  • drawerContent: Defines the visual UI that will appear inside the side panel when opened.
  • content: Defines the regular visible screen of the user over which the Drawer will display (where we will place our Scaffold).

The Drawer content: ModalDrawerSheet and NavigationDrawerItem

Inside the drawerContent parameter, we use Material 3's ModalDrawerSheet container. This component automatically applies the recommended dimensions, container tonal color, elevation, and rounded corners on the outer edge of the menu.

Inside ModalDrawerSheet, we structure the elements making up the side menu:

  • Header: You can place a Text with prominent typography styles like MaterialTheme.typography.titleLarge, complemented by a Spacer(Modifier.height(12.dp)) to leave an adequate top margin relative to the status bar.
  • Dividers: The HorizontalDivider() composable helps visually separate the header from options or create clear boundaries between navigation groups.
  • Navigation items: To represent each destination, we iterate through a list of options using the specialized NavigationDrawerItem component.

Each NavigationDrawerItem features highly intuitive properties:

  • label: The text label describing the option (e.g., label = { Text("Home") }).
  • selected: Boolean indicating whether the current option is the selected one (selected = index == selectedItem).
  • onClick: Callback executed when the user taps the item. Here we update the state selectedItem = index and close the menu by launching a coroutine with scope.launch { drawerState.close() }.
  • icon (optional): Composable to render a descriptive icon next to the text.
  • badge (optional): Badge to show counters or informational labels (such as pending notifications).
  • modifier: Allows adding standard padding with Modifier.padding(NavigationDrawerItemDefaults.ItemPadding) to achieve Material 3's rounded pill appearance.

Let's see how ModalNavigationDrawer is assembled with its ModalDrawerSheet:

val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val scope = rememberCoroutineScope()
var selectedItem by remember { mutableIntStateOf(0) }
val items = listOf("Home", "Profile", "Settings")

// Main Drawer container
ModalNavigationDrawer(
    drawerState = drawerState,
    drawerContent = {
        // Visual content of the side menu
        ModalDrawerSheet {
            Spacer(Modifier.height(12.dp))
            Text(
                text = "My 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
                        // Close the drawer after selecting
                        scope.launch { drawerState.close() }
                    },
                    modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
                )
            }
        }
    }
) {
    // Here we will place the Scaffold with the main screen
}

The Scaffold and the Top Bar (TopAppBar)

Once the side panel is defined in drawerContent, we place the Scaffold in the main body of the ModalNavigationDrawer. The Scaffold provides the base structure for laying out the top bar and content area:

  • TopAppBar: In the top bar we display the title of the active screen (which can vary dynamically based on the chosen option via items[selectedItem]) and the navigation icon.
  • Open event: In the navigationIcon parameter, we place an IconButton with the Icons.Default.Menu icon. When clicked, we execute scope.launch { drawerState.open() } to open the side menu with its corresponding animation.
  • Main content: The Scaffold provides a padding object (of type PaddingValues) that must be applied using Modifier.padding(padding) to the inner container (such as a Box or Column) to ensure the view respects system bars and does not get hidden under the TopAppBar.
ModalNavigationDrawer(
    drawerState = drawerState,
    drawerContent = {
        // ModalDrawerSheet with navigation items...
    }
) {
    // Main screen content
    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("${items[selectedItem]} Screen") },
                navigationIcon = {
                    IconButton(onClick = {
                        // Open the drawer when clicking the hamburger menu icon
                        scope.launch { drawerState.open() }
                    }) {
                        Icon(Icons.Default.Menu, contentDescription = "Open navigation menu")
                    }
                }
            )
        }
    ) { padding ->
        Box(
            modifier = Modifier
                .fillMaxSize()
                .padding(padding),
            contentAlignment = Alignment.Center
        ) {
            Text(text = "Content for ${items[selectedItem]}")
        }
    }
}

Static vs. Dynamic Menus and Complete Examples in Jetpack Compose

There are two common strategies when populating options in your Navigation Drawer depending on the project's complexity:

  • Fixed or Static Implementation: Options are declared directly as a list in code (for example, listOf("Home", "Profile", "Settings")). It is a quick technique well-suited for static settings screens or menus that do not vary across sessions.
  • Dynamic Implementation: Elements come from a data model managed by a ViewModel or a REST API. This approach allows displaying numeric badges with real-time notifications, filtering destinations based on logged-in user permissions or roles, and associating dynamic icons with each item.

If you need to organize thematic submenus within your side menu, simply add section titles with Text (using styles like MaterialTheme.typography.titleMedium) and delimit them with HorizontalDivider.

Below is the complete code for MainActivity, including both a basic functional example and an advanced example (DetailedDrawerExample) featuring icons, count badges (badge), submenus, and vertical scroll support via 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()
                /*
                // Alternative with detailed Drawer and custom content:
                DetailedDrawerExample { paddingValues ->
                    Column(
                        modifier = Modifier
                            .fillMaxSize()
                            .padding(paddingValues)
                    ) {
                        Text("Hello! I am inside the screen body")
                        Button(onClick = { /* Action */ }) {
                            Text("Test Button")
                        }
                    }
                }
                */
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EjemploDrawerCompleto() {
    // 1. State to open/close the Drawer
    val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
    val scope = rememberCoroutineScope()

    // 2. State to track which item is selected
    var selectedItem by remember { mutableIntStateOf(0) }
    val items = listOf("Home", "Profile", "Settings")

    // 3. The main Drawer container
    ModalNavigationDrawer(
        drawerState = drawerState,
        drawerContent = {
            // Visual content of the side menu
            ModalDrawerSheet {
                Spacer(Modifier.height(12.dp))
                Text(
                    text = "My 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
                            // Close the drawer after clicking
                            scope.launch { drawerState.close() }
                        },
                        modifier = Modifier.padding(NavigationDrawerItemDefaults.ItemPadding)
                    )
                }
            }
        }
    ) {
        // 4. Main screen content with Scaffold
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("${items[selectedItem]} Screen") },
                    navigationIcon = {
                        IconButton(onClick = {
                            // Open drawer when tapping hamburger menu icon
                            scope.launch { drawerState.open() }
                        }) {
                            Icon(Icons.Default.Menu, contentDescription = "Open menu")
                        }
                    }
                )
            }
        ) { padding ->
            Box(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(padding),
                contentAlignment = Alignment.Center
            ) {
                Text(text = "Content for ${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 = "Navigation Panel",
                        modifier = Modifier.padding(16.dp),
                        style = MaterialTheme.typography.titleLarge
                    )
                    HorizontalDivider()

                    // Section 1 options
                    Text(
                        text = "Main",
                        modifier = Modifier.padding(16.dp),
                        style = MaterialTheme.typography.titleMedium
                    )
                    NavigationDrawerItem(
                        label = { Text("Home") },
                        selected = false,
                        onClick = { /* Handle navigation to Home */ }
                    )
                    NavigationDrawerItem(
                        label = { Text("Favorites") },
                        selected = false,
                        onClick = { /* Handle navigation to Favorites */ }
                    )

                    HorizontalDivider(modifier = Modifier.padding(vertical = 8.dp))

                    // Section 2 options (Submenu with icons and badges)
                    Text(
                        text = "Preferences",
                        modifier = Modifier.padding(16.dp),
                        style = MaterialTheme.typography.titleMedium
                    )
                    NavigationDrawerItem(
                        label = { Text("Settings") },
                        selected = false,
                        icon = { Icon(Icons.Outlined.Settings, contentDescription = null) },
                        badge = { Text("20") }, // Numeric badge indicator
                        onClick = { /* Handle settings click */ }
                    )
                    NavigationDrawerItem(
                        label = { Text("Help & support") },
                        selected = false,
                        icon = { Icon(Icons.AutoMirrored.Outlined.Help, contentDescription = null) },
                        onClick = { /* Handle help click */ }
                    )
                    Spacer(Modifier.height(12.dp))
                }
            }
        }
    ) {
        Scaffold(
            topBar = {
                TopAppBar(
                    title = { Text("Advanced Navigation Drawer") },
                    navigationIcon = {
                        IconButton(onClick = {
                            scope.launch {
                                if (drawerState.isClosed) {
                                    drawerState.open()
                                } else {
                                    drawerState.close()
                                }
                            }
                        }) {
                            Icon(Icons.Default.Menu, contentDescription = "Menu")
                        }
                    }
                )
            }
        ) { innerPadding ->
            content(innerPadding)
        }
    }
}

In Jetpack Compose, the user interface is a direct representation of state. Thanks to this reactive paradigm, the view responds smoothly to drag touch gestures, while programmatic transitions execute cleanly using coroutines.

Navigation Drawer in XML and Classic Views (Traditional Approach)

Although Jetpack Compose is the present and future of Android development, many existing applications continue to be based on the traditional view system using XML files. Knowing how to structure a side menu using DrawerLayout and NavigationView is essential if you work on maintaining or gradually modernizing an existing project.

The side menu via DrawerLayout once represented a major step forward compared to the classic options menu in the top action bar:

traditional menu in android

Creating a Side Menu with NavigationView in XML

With the arrival of the Material Design library (now part of AndroidX), Google standardized side menu creation through the NavigationView component, eliminating the need to assemble manual adapters with ListView:

Final side menu

In this section, we will see how to set up a modern Navigation Drawer in XML by combining DrawerLayout and NavigationView. If you need to consult the original legacy implementation based on ListView, you can jump directly to the legacy ListView section.

In your activity's XML layout (e.g., activity_main.xml), we define the basic hierarchy using 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">

    <!-- Main activity content (FrameLayout, CoordinatorLayout, etc.) -->

    <!-- Side menu with 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>

As you can observe, the structure consists of two key parts:

  • DrawerLayout: The top-level root container. It holds both the main screen content (buttons, text, fragments) and the sliding panel.
  • NavigationView: Represents the visual panel of the side menu. It is responsible for rendering the header and option list from declarative XML resources.

Key Properties of NavigationView

If we analyze the code above, several essential attributes stand out:

  • android:layout_gravity="start": Defines which side the menu slides out from. The value start is the recommended best practice because it respects the system's reading direction (left for LTR languages and right for RTL languages).
  • android:fitsSystemWindows="true": Allows the side menu to draw harmoniously behind system bars (such as the status bar).
  • app:menu="@menu/menu": Binds the menu resource file defining navigation items.
  • app:headerLayout="@layout/nav_header": Specifies a custom layout for the menu header.

The menu file (res/menu/menu.xml) defines the titles and icons corresponding to each option:

<?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>

In this menu, each <item> tag has a unique identifier (android:id), descriptive text (android:title), and a graphic resource assigned as an icon (android:icon).

Defining a Header Layout

Using the app:headerLayout property, we can reference a separate layout (e.g., res/layout/nav_header.xml) to design the Drawer header. It typically includes the user's avatar, name, and email address:

<?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="User Name" />

    <TextView 
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#FFF"
        android:gravity="center"
        android:text="user@email.com" />

</LinearLayout>

The visual result of the header looks as follows:

Side menu header

By integrating the NavigationView with its header and menu file, we get a fully functional navigation drawer:

Final side menu second example

Groups and submenus in the XML side menu

Groupings using the <group> tag are very useful when we want to organize options by topic or category. In addition to introducing an automatic divider line, they allow enabling or disabling entire sections programmatically from Java or Kotlin code:

<group android:id="@+id/group1">
    <item 
        android:id="@+id/op3_1"
        android:title="Option 3_1"
        android:icon="@drawable/ic_attach_file" />
    <item 
        android:id="@+id/op3_2"
        android:title="Option 3_2"
        android:icon="@drawable/ic_attach_file" />
</group>

Obtaining the following structured display:

Final side menu second example

Selectable behavior (checkableBehavior) and nested submenus

We can require that only one item remains selected at a time within a group by applying the property android:checkableBehavior="single" (radio button-style behavior). Likewise, it is possible to nest <menu> tags inside an <item> to create submenus with their own subtitle:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
    <item 
        android:id="@+id/op1"
        android:title="Option 1"
        android:icon="@drawable/ic_android" />
    <item 
        android:id="@+id/op2"
        android:title="Option 2"
        android:icon="@drawable/ic_android" />
    <item 
        android:id="@+id/op3"
        android:title="Option 3"
        android:icon="@drawable/ic_attach_file" />

    <group android:id="@+id/group1">
        <item 
            android:id="@+id/op3_1"
            android:title="Option 3_1"
            android:icon="@drawable/ic_attach_file" />
        <item 
            android:id="@+id/op3_2"
            android:title="Option 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="Another Group">
            <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>

With this configuration, we achieve segmented submenus with independent header titles:

Final side menu second example

Creating the NavigationDrawer with ListView (Legacy Historical Approach)

This section documents the historical implementation of a side menu using ListView and manual adapters. For new projects using traditional XML views, NavigationView or RecyclerView is recommended; however, this method continues to be of great educational value for understanding the internal mechanics of DrawerLayout:

1. Creating the Drawer Layout with ListView

In this approach, we add the DrawerLayout as the root element of the activity, placing inside it the main content view and a ListView that will act as the sliding side menu panel:

<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">

    <!-- Main activity content -->
    <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="Hello World!" />
    </RelativeLayout>

    <!-- Side menu using 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>

The regular content of the activity must be placed as the first child element inside androidx.drawerlayout.widget.DrawerLayout.

We can specify the direction from which the menu slides out by modifying the android:layout_gravity attribute:

  • android:layout_gravity="left" (or start):
left-aligned side menu in android
  • android:layout_gravity="right" (or end):
right-aligned side menu in android

By guideline and Material Design convention, the standard position for the main menu is on the left side (start or left). However, placing it on the right side (end or right) is very useful if you need to implement a secondary panel for filters or complementary actions.

2. Referencing the Drawer Layout and ListView in code

Once the XML is defined, in our activity's onCreate() method we obtain references to the ListView and the DrawerLayout:

ListView listView = (ListView) findViewById(R.id.list_view);
DrawerLayout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout);

3. Defining the Adapter with the list of options

With the views referenced, we create the array of destinations and bind it to the ListView using an Adapter such as ArrayAdapter:

String[] opciones = { "Option 1", "Option 2", "Option 3", "Option 4" };
listView.setAdapter(new ArrayAdapter<String>(
    this,
    android.R.layout.simple_list_item_1, 
    android.R.id.text1,
    opciones
));

4. Configuring open and close events with the application icon

To allow the user to open and close the menu by pressing the action bar icon at the top, we must enable the navigation button and intercept the selection of the android.R.id.home item inside onOptionsItemSelected():

// Enable the navigation button in the action bar:
// getActionBar().setDisplayHomeAsUpEnabled(true);

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case android.R.id.home:
            // Check if the navigation drawer is open or closed
            if (drawerLayout.isDrawerOpen(listView)) {
                drawerLayout.closeDrawers();
            } else {
                drawerLayout.openDrawer(listView);
            }
            return true;
    }
    return super.onOptionsItemSelected(item);
}

Conclusion: Which approach should you use?

The evolution of the Navigation Drawer in Android reflects the advancement of development tools on the platform:

  • For new projects: Always use Jetpack Compose with ModalNavigationDrawer, ModalDrawerSheet, and NavigationDrawerItem. Its declarative approach drastically reduces code lines, eliminates the need for auxiliary XML files, and automatically handles visual states and Material 3 themes.
  • For existing XML projects: The DrawerLayout combined with NavigationView remains the standard, robust solution for maintaining traditional view-based architectures.

Next step: learn how to create your Tab system in Jetpack Compose with HorizontalPager.

Learn how to create a Navigation Drawer (side menu) in Android with Jetpack Compose and Material 3—using ModalNavigationDrawer, DrawerState, coroutines, and ModalDrawerSheet—step by step.


Ú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:

I agree to receive announcements of interest about this Blog.