The Jetpack Compose navigation drawer provides a convenient way to navigate between different sections of your application. This powerful component slides in from the edge of the screen, typically from the left side, revealing a menu of navigation options. The Jetpack Compose navigation drawer follows Material Design guidelines and can be customized to match your app's theme and user experience requirements.
Before diving into implementation, let's understand the key components involved in creating a Jetpack Compose navigation drawer:
To implement a Jetpack Compose navigation drawer, we need to add the necessary dependencies to our project. Let's start with the required dependencies in your build.gradle
file:
dependencies {
// Core Jetpack Compose dependencies
implementation "androidx.compose.ui:ui:1.5.4"
implementation "androidx.compose.material:material:1.5.4"
implementation "androidx.compose.ui:ui-tooling-preview:1.5.4"
// Navigation Component for Compose
implementation "androidx.navigation:navigation-compose:2.7.5"
// Optional: Material icons
implementation "androidx.compose.material:material-icons-extended:1.5.4"
}
Let's start with a basic implementation of a Jetpack Compose navigation drawer. This example will show you how to create a simple drawer with navigation items:
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
fun BasicNavigationDrawer() {
// Create a scaffold state to control the drawer
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = { Text("Jetpack Compose Navigation Drawer") },
navigationIcon = {
IconButton(onClick = {
// Open the drawer when the menu icon is clicked
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(Icons.Filled.Menu, contentDescription = "Menu")
}
}
)
},
drawerContent = {
// Content for the navigation drawer
Column(
modifier = Modifier.fillMaxHeight().padding(16.dp)
) {
Text(
text = "Navigation Drawer",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 24.dp)
)
// Navigation Item: Home
DrawerItem(
icon = Icons.Filled.Home,
label = "Home",
onClick = {
scope.launch {
// Close drawer and navigate to Home
scaffoldState.drawerState.close()
// Navigation logic would go here
}
}
)
Spacer(modifier = Modifier.height(16.dp))
// Navigation Item: Settings
DrawerItem(
icon = Icons.Filled.Settings,
label = "Settings",
onClick = {
scope.launch {
// Close drawer and navigate to Settings
scaffoldState.drawerState.close()
// Navigation logic would go here
}
}
)
}
},
content = {
// Main content of your screen
Box(
modifier = Modifier.fillMaxSize().padding(it),
contentAlignment = Alignment.Center
) {
Text("Main Content Area")
}
}
)
}
@Composable
fun DrawerItem(
icon: ImageVector,
label: String,
onClick: () -> Unit
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(vertical = 8.dp)
) {
Icon(
imageVector = icon,
contentDescription = label,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = label,
style = MaterialTheme.typography.body1
)
}
}
This basic example demonstrates how to create a simple Jetpack Compose navigation drawer with menu items. Let's explore some of the key properties and customizations you can apply to enhance your Jetpack Compose navigation drawer.
The Jetpack Compose navigation drawer can be customized in many ways to match your app's design requirements. Here are some customization options:
You can adjust the width of your Jetpack Compose navigation drawer:
drawerContent = {
Column(
modifier = Modifier
.fillMaxHeight()
.width(300.dp) // Custom drawer width
.padding(16.dp)
) {
// Drawer content
}
}
Customize the background color of your Jetpack Compose navigation drawer:
drawerContent = {
Surface(
color = MaterialTheme.colors.surface, // Or any custom color
modifier = Modifier.fillMaxHeight()
) {
Column(modifier = Modifier.padding(16.dp)) {
// Drawer content
}
}
}
You can also customize the shape of your Jetpack Compose navigation drawer:
Scaffold(
drawerShape = RoundedCornerShape(topEnd = 16.dp, bottomEnd = 16.dp),
// other properties
)
A Jetpack Compose navigation drawer is most useful when integrated with the Navigation Component. Let's implement a navigation drawer with actual screen navigation:
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch
@Composable
fun NavigationDrawerWithNavigation() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val navController = rememberNavController()
// Track the current screen to update the top bar title
var currentScreen by remember { mutableStateOf("Home") }
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = { Text(currentScreen) },
navigationIcon = {
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(Icons.Filled.Menu, contentDescription = "Menu")
}
}
)
},
drawerContent = {
DrawerHeader()
DrawerBody(
navController = navController,
scaffoldState = scaffoldState,
scope = scope,
onDestinationChanged = { screen ->
currentScreen = screen
}
)
},
content = { paddingValues ->
NavHost(
navController = navController,
startDestination = "home",
modifier = Modifier.padding(paddingValues)
) {
composable("home") {
HomeScreen()
}
composable("profile") {
ProfileScreen()
}
composable("settings") {
SettingsScreen()
}
composable("help") {
HelpScreen()
}
}
}
)
}
@Composable
fun DrawerHeader() {
Box(
modifier = Modifier
.fillMaxWidth()
.height(180.dp)
.padding(vertical = 16.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
// You can add an app logo or user profile image here
Surface(
modifier = Modifier.size(80.dp),
shape = MaterialTheme.shapes.circle,
color = MaterialTheme.colors.primary
) {
// Placeholder for logo/profile image
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "My Compose App",
style = MaterialTheme.typography.h6
)
Text(
text = "example@email.com",
style = MaterialTheme.typography.body2
)
}
}
Divider()
}
@Composable
fun DrawerBody(
navController: NavController,
scaffoldState: ScaffoldState,
scope: CoroutineScope,
onDestinationChanged: (String) -> Unit
) {
val menuItems = listOf(
MenuItem(
title = "Home",
icon = Icons.Default.Home,
route = "home"
),
MenuItem(
title = "Profile",
icon = Icons.Default.Person,
route = "profile"
),
MenuItem(
title = "Settings",
icon = Icons.Default.Settings,
route = "settings"
),
MenuItem(
title = "Help",
icon = Icons.Default.Info,
route = "help"
)
)
Column(modifier = Modifier.padding(16.dp)) {
menuItems.forEach { item ->
DrawerItem(
icon = item.icon,
label = item.title,
onClick = {
navController.navigate(item.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
popUpTo(navController.graph.startDestinationId)
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
}
// Update the current screen title
onDestinationChanged(item.title)
// Close the drawer
scope.launch {
scaffoldState.drawerState.close()
}
}
)
Spacer(modifier = Modifier.height(16.dp))
}
}
}
// Data class for menu items
data class MenuItem(
val title: String,
val icon: ImageVector,
val route: String
)
// Example screen composables
@Composable
fun HomeScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Home Screen Content")
}
}
@Composable
fun ProfileScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Profile Screen Content")
}
}
@Composable
fun SettingsScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Settings Screen Content")
}
}
@Composable
fun HelpScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text("Help Screen Content")
}
}
The Jetpack Compose navigation drawer supports gesture-based interactions. Users can swipe from the edge of the screen to open the drawer or swipe the drawer to close it. The drawerGesturesEnabled
property allows you to control this behavior:
Scaffold(
scaffoldState = scaffoldState,
drawerGesturesEnabled = true, // Enable or disable drawer gestures
// other properties
)
Jetpack Compose offers two types of navigation drawers:
Here's how you can implement a permanent navigation drawer for larger screens:
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun PermanentNavigationDrawerExample() {
Row {
// Permanent navigation drawer - always visible
Surface(
modifier = Modifier
.width(240.dp)
.fillMaxHeight(),
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Permanent Drawer",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 24.dp)
)
// Drawer items go here
}
}
// Main content
Box(
modifier = Modifier.fillMaxSize()
) {
Text("Main Content")
}
}
}
For a responsive design, you might want to show a permanent drawer on larger screens and a modal drawer on smaller screens:
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@Composable
fun ResponsiveNavigationDrawer() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val configuration = LocalConfiguration.current
// Determine if we should use permanent or modal drawer based on screen width
val isExpandedScreen = configuration.screenWidthDp >= 600
if (isExpandedScreen) {
// Use permanent drawer for larger screens
Row {
Surface(
modifier = Modifier
.width(240.dp)
.fillMaxHeight(),
elevation = 4.dp
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Navigation Drawer",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 24.dp)
)
// Drawer items
}
}
// Main content
Box(modifier = Modifier.fillMaxSize()) {
Text("Main Content")
}
}
} else {
// Use modal drawer for smaller screens
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = { Text("Jetpack Compose Navigation Drawer") },
navigationIcon = {
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(Icons.Filled.Menu, contentDescription = "Menu")
}
}
)
},
drawerContent = {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Navigation Drawer",
style = MaterialTheme.typography.h6,
modifier = Modifier.padding(bottom = 24.dp)
)
// Drawer items
}
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
Text("Main Content")
}
}
}
}
Let's put everything together into a complete, working example that you can use in your Android app:
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposeNavigationDrawerTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
ComposeNavigationDrawerApp()
}
}
}
}
}
@Composable
fun ComposeNavigationDrawerTheme(content: @Composable () -> Unit) {
MaterialTheme(
colors = lightColors(
primary = Color(0xFF6200EE),
primaryVariant = Color(0xFF3700B3),
secondary = Color(0xFF03DAC6)
),
content = content
)
}
@Composable
fun ComposeNavigationDrawerApp() {
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
val navController = rememberNavController()
val configuration = LocalConfiguration.current
// Determine if we should use permanent or modal drawer based on screen width
val isExpandedScreen = configuration.screenWidthDp >= 600
// Navigation items
val navItems = listOf(
NavItem("Home", Icons.Default.Home, "home"),
NavItem("Profile", Icons.Default.Person, "profile"),
NavItem("Settings", Icons.Default.Settings, "settings"),
NavItem("Favorites", Icons.Default.Favorite, "favorites"),
NavItem("Help", Icons.Default.Info, "help")
)
// Get current route to highlight the active item
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.destination?.route
if (isExpandedScreen) {
// Permanent drawer layout for larger screens
Row(modifier = Modifier.fillMaxSize()) {
PermanentDrawerContent(
navItems = navItems,
currentRoute = currentRoute,
onNavItemClick = { route ->
navigateToScreen(navController, route)
}
)
Box(modifier = Modifier.weight(1f)) {
NavigationContent(navController = navController)
}
}
} else {
// Modal drawer layout for smaller screens
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = {
Text(
text = navItems.find { it.route == currentRoute }?.title ?: "Compose App",
)
},
navigationIcon = {
IconButton(onClick = {
scope.launch {
scaffoldState.drawerState.open()
}
}) {
Icon(Icons.Default.Menu, contentDescription = "Menu")
}
}
)
},
drawerContent = {
ModalDrawerContent(
navItems = navItems,
currentRoute = currentRoute,
onNavItemClick = { route ->
navigateToScreen(navController, route)
scope.launch {
scaffoldState.drawerState.close()
}
}
)
}
) { paddingValues ->
Box(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
NavigationContent(navController = navController)
}
}
}
}
@Composable
fun PermanentDrawerContent(
navItems: List<NavItem>,
currentRoute: String?,
onNavItemClick: (String) -> Unit
) {
Surface(
modifier = Modifier
.width(240.dp)
.fillMaxHeight(),
elevation = 4.dp
) {
Column(
modifier = Modifier.padding(vertical = 24.dp)
) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
text = "Navigation Drawer",
style = MaterialTheme.typography.h6
)
}
Spacer(modifier = Modifier.height(32.dp))
navItems.forEach { item ->
val isSelected = currentRoute == item.route
Surface(
color = if (isSelected) MaterialTheme.colors.primary.copy(alpha = 0.12f) else Color.Transparent,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onNavItemClick(item.route) }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = item.icon,
contentDescription = item.title,
tint = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = item.title,
style = MaterialTheme.typography.body1,
color = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
)
}
}
Spacer(modifier = Modifier.height(4.dp))
}
}
}
}
@Composable
fun ModalDrawerContent(
navItems: List<NavItem>,
currentRoute: String?,
onNavItemClick: (String) -> Unit
) {
Column(
modifier = Modifier.fillMaxHeight()
) {
Box(
modifier = Modifier
.fillMaxWidth()
.height(180.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Surface(
modifier = Modifier.size(72.dp),
shape = MaterialTheme.shapes.circle,
color = MaterialTheme.colors.primary
) {
// App logo or user avatar
}
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Compose Drawer App",
style = MaterialTheme.typography.h6
)
Text(
text = "example@email.com",
style = MaterialTheme.typography.body2,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
)
}
}
Divider()
Spacer(modifier = Modifier.height(16.dp))
navItems.forEach { item ->
val isSelected = currentRoute == item.route
Surface(
color = if (isSelected) MaterialTheme.colors.primary.copy(alpha = 0.12f) else Color.Transparent,
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onNavItemClick(item.route) }
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = item.icon,
contentDescription = item.title,
tint = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
)
Spacer(modifier = Modifier.width(16.dp))
Text(
text = item.title,
style = MaterialTheme.typography.body1,
color = if (isSelected) MaterialTheme.colors.primary else LocalContentColor.current
)
}
}
Spacer(modifier = Modifier.height(4.dp))
}
}
}
@Composable
fun NavigationContent(navController: NavController) {
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
ScreenContent(
title = "Home Screen",
backgroundColor = Color(0xFFE3F2FD)
)
}
composable("profile") {
ScreenContent(
title = "Profile Screen",
backgroundColor = Color(0xFFE8F5E9)
)
}
composable("settings") {
ScreenContent(
title = "Settings Screen",
backgroundColor = Color(0xFFFFF3E0)
)
}
composable("favorites") {
ScreenContent(
title = "Favorites Screen",
backgroundColor = Color(0xFFF3E5F5)
)
}
composable("help") {
ScreenContent(
title = "Help Screen",
backgroundColor = Color(0xFFEFEBE9)
)
}
}
}
@Composable
fun ScreenContent(
title: String,
backgroundColor: Color
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = backgroundColor
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = title,
style = MaterialTheme.typography.h4,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "This is the content for the $title",
style = MaterialTheme.typography.body1,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 32.dp)
)
}
}
}
data class NavItem(
val title: String,
val icon: ImageVector,
val route: String
)
private fun navigateToScreen(navController: NavController, route: String) {
navController.navigate(route) {
// Pop up to start destination to avoid building up a large stack
popUpTo(navController.graph.startDestinationId)
// Avoid multiple copies of the same destination
launchSingleTop = true
}
}