Navigation drawer in Android Jetpack Compose (Material Design 3)

Meet Patadia
10 min readDec 3, 2023

--

In the fast-paced world of Android development, staying abreast of the latest technologies is paramount to creating modern, intuitive, and visually stunning applications. One design element that plays a pivotal role in enhancing user experience is the Navigation Drawer. With the advent of Material Design 3 and the power of Jetpack Compose, implementing a sophisticated Navigation Drawer has become not only essential but also more accessible than ever.

Unveiling the Navigation Drawer

The Navigation Drawer is more than just a UI element; it’s a critical component that contributes to the overall user experience. Here are some key reasons why the Navigation Drawer holds such significance in Android applications:

1. Intuitive Navigation:

Users expect a seamless and intuitive way to navigate through different sections of an app. The Navigation Drawer provides a standardized mechanism for accessing various screens with minimal effort.

2. Consistency Across Screens:

The drawer’s persistent presence ensures consistency in navigation patterns across different screens, contributing to a cohesive and user-friendly app experience.

3. Optimal Use of Screen Real Estate:

With the proliferation of diverse screen sizes and aspect ratios, the Navigation Drawer allows efficient utilization of screen real estate. It provides a space-saving solution while accommodating a growing number of app features.

4. Enhanced Discoverability:

By presenting a concise list of app sections or features, the Navigation Drawer enhances discoverability, helping users quickly locate and access the content they seek.

In the upcoming sections, we’ll dive into the implementation details of a Navigation Drawer using Jetpack Compose and Material Design 3, exploring the provided code and unraveling the intricacies of crafting a sleek and functional navigation experience for your Android application. Get ready to elevate your app’s navigation game to new heights!

Overview of the Navigation Drawer Implementation

The key components are:

1. build.gradle.kts (:app):
We will add two dependencies here, one for Material design 3 and second for navigation as we are going to use navigation component to navigate between two screens.

2. ActiveScreen.kt:
This file defines a sealed class ActiveScreen that encapsulates information about different screens in the app. Two screens, "Home" and "About," are represented as objects with properties like icon, content description, title, and route. This structured approach allows for easy management and extension of screens.

3. MainScreen:
The MainScreen composable is the heart of the app. Here's an overview of its features:

  • Navigation Setup: It initializes a NavController and a DrawerState to manage the navigation and drawer state, respectively.
  • Drawer Content: The content of the drawer is defined using Jetpack Compose components. It includes a title/header and a list of navigation items (NavigationDrawerItem). Icons, labels, and navigation actions are dynamically generated based on the navDrawerItem list.
  • Navigation Actions: When a drawer item is clicked, it triggers navigation actions. The NavController is used to navigate to the specified route. Additionally, the code includes considerations for handling navigation back stack and ensuring a smooth user experience.

4. DrawerNavGraph.kt:
This file sets up the navigation graph for the app. It defines two destinations, “Home” and “About,” associating each with its respective composable (HomeScreen() and AboutScreen()). The starting destination is set to "Home."

4. CenterAlignedToolbar.kt:
This file introduces a CenterAlignedToolbar composable, providing a clean and centralized toolbar with navigation menu functionality.

So after understanding overview of code and flow of code, let’s dive into actual code now!!

Beneath the Code Hood: Crafting the Navigation Drawer Magic

Alright, code wizards and keyboard rockstars, buckle up your binary belts because it’s time to breakdance through the actual code! Let’s flip some bits and drop some code bombs! We’ll go through each line of code, and I’ll try my best to explain them in detail.

Starting with build.gradle.kts (:app)

implementation("androidx.compose.material3:material3:1.2.0-alpha12")
implementation("androidx.navigation:navigation-compose:2.7.5")

Now that we’ve added the required dependencies to our project, let’s sync the project to make sure everything is up-to-date. Once the sync is complete, we’ll embark on creating a Kotlin sealed class named ActiveScreen. This class is the backbone of our navigation drawer, holding information about the different screens in our app. Let's dive in and explore the contents of this essential file.

import com.codewithzebru.composedemo.R

sealed class ActiveScreen(
val icon: Int,
val contentDescription: String,
val title: String,
val route: String
) {
object Home: ActiveScreen(
icon = R.drawable.round_home_24,
contentDescription = "Go to home screen",
title = "Home",
route = "home_screen"
)

object About: ActiveScreen(
icon = R.drawable.outline_info_24,
contentDescription = "Go to about screen",
title = "About",
route = "about_screen"
)
}
  • ActiveScreen is a sealed class, which means all subclasses must be declared in the same file where the sealed class is declared.
  • It has four properties: icon (resource ID for the screen's icon), contentDescription (a description of the screen for accessibility purposes), title (the title or name of the screen), and route (the route string associated with the screen).
  • Two objects (Home and About) are declared as subclasses of ActiveScreen. These represent distinct screens in the app.
  • Home has an icon representing a home, a content description, the title "Home," and the route "home_screen."
  • About has an icon representing information, a content description, the title "About," and the route "about_screen."

Alright, that’s it for ActiveSreen.kt. Let’s take a closer look at the MainScreen.kt, which plays a crucial role in implementing the navigation drawer functionality.

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.compose.rememberNavController
import kotlinx.coroutines.launch

@Composable
fun MainScreen() {
val drawerNavController = rememberNavController()
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
val coroutineScope = rememberCoroutineScope()
val navDrawerItem = listOf(
ActiveScreen.Home,
ActiveScreen.About
)

ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
ModalDrawerSheet(
modifier = Modifier.width(280.dp)
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
// HEADER - TITLE
Text(
text = stringResource(id = R.string.app_name),
fontSize = 18.sp,
fontWeight = FontWeight.W500,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)

// NAVIGATION ITEMS
navDrawerItem.forEachIndexed { index, activeScreen ->
NavigationDrawerItem(
icon = {
Image(
painter = painterResource(id = activeScreen.icon),
contentDescription = activeScreen.contentDescription
)
},
label = {
Text(
text = activeScreen.title,
color = MaterialTheme.colorScheme.onBackground,
fontWeight = FontWeight.W400
)
},
selected = false,
onClick = {
coroutineScope.launch { drawerState.close() }
if (index == 0) {
drawerNavController.navigate(activeScreen.route) {
drawerNavController.graph.startDestinationRoute.let {
popUpTo(it!!) {
saveState = true
}
}
restoreState = true
}
} else if (index == 1) {
drawerNavController.navigate(activeScreen.route) {
drawerNavController.graph.startDestinationRoute.let {
popUpTo(it!!) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
}
}
) // navigation-drawer-item
}
} // column
} // model-drawer-sheet
} // model-navigation-drawer
) {
Scaffold(
topBar = { CenterAlignedToolbar(drawerState, coroutineScope) }
) {
Box(Modifier.padding(it)) {
DrawerNavGraph(drawerNavController)
}
}
}
}

Feeling a bit overwhelmed? Fear not! Let’s break down this intricate piece of code into bite-sized chunks and unravel its mysteries together. Let’s simplify the complexity step by step.

  • The MainScreen composable starts by initializing some important variables required for handling the navigation drawer. This includes creating a navigation controller (drawerNavController), managing the drawer state (drawerState), and setting up a coroutine scope (coroutineScope).
  • The ModalNavigationDrawer is a fundamental part of the Material Design 3 setup, providing the modal drawer for navigation. It takes the drawer state and the drawer content as parameters. The drawer content is defined within the ModalDrawerSheet, where you find the header and navigation items.
  • Inside the ModalDrawerSheet, a Column is used to arrange the content vertically. It includes a header displaying the app title (you can add image as well) and a list of navigation items. Each item is represented by a NavigationDrawerItem, containing an icon, label, and an onClick action to handle navigation.
  • The onClick action inside each NavigationDrawerItem handles the navigation when an item is clicked. It closes the drawer, and based on the index of the clicked item, it navigates to the corresponding screen using the drawerNavController. Special considerations are made for preserving and restoring the navigation state.
  • The Scaffold composable structures the app's UI with a centered toolbar, a padded content area, and an integrated navigation graph for the drawer. The centered toolbar, driven by CenterAlignedToolbar, positions content elegantly. Enclosed in a padded Box, the navigation graph (DrawerNavGraph) ensures smooth navigation between drawer destinations. It's the foundation for a cohesive user experience.

We’ve woven the threads of the navigation drawer, complete with its header and enchanting items. But wait, the magic doesn’t end there. Now, it’s time to unveil the secret passages that guide our journey through screens. Let’s dive into the DrawerNavGraph.kt to connect the dots and discover how navigation is seamlessly orchestrated.

import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable

@Composable
fun DrawerNavGraph(drawerNavController: NavHostController) {
NavHost(navController = drawerNavController, startDestination = ActiveScreen.Home.route) {
composable(ActiveScreen.Home.route) {
HomeScreen()
}

composable(ActiveScreen.About.route) {
AboutScreen()
}
}
}
  • The DrawerNavGraph function takes a NavHostController as a parameter, which is responsible for handling navigation within the drawer.
  • The NavHost sets up the navigation host for the drawer. It uses the provided drawerNavController as the controller and specifies the startDestination as the home screen's route from the ActiveScreen sealed class.
  • Two composable functions define the destinations within the navigation host. When the navigation controller reaches the home screen or about screen routes, it composes the corresponding HomeScreen() or AboutScreen() composable.

And our CenterAlignedToolbar

import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.DrawerState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CenterAlignedToolbar(drawerState: DrawerState, coroutineScope: CoroutineScope) {
CenterAlignedTopAppBar(
title = {
Text(
text = stringResource(id = R.string.app_name),
fontWeight = FontWeight.SemiBold,
style = MaterialTheme.typography.titleLarge
)
}, // title
navigationIcon = {
Image(
imageVector = Icons.Rounded.Menu,
contentDescription = "Navigation menu",
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onPrimary),
modifier = Modifier
.padding(start = 20.dp)
.size(31.dp)
.clickable {
coroutineScope.launch {
drawerState.apply {
if (isClosed) open() else close()
}
}
},
)
}, // navigation-icon
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = MaterialTheme.colorScheme.primary,
titleContentColor = MaterialTheme.colorScheme.onPrimary
)
) // center-aligned-top-app-bar
}
  • CenterAlignedTopAppBar: Composable for creating a top app bar with a centered title and customizable navigation icon.
  • title Parameter: Displays the app name using the Text composable with specified styling.
  • navigationIcon Parameter: Utilizes the Image composable to represent the navigation menu icon, enabling users to toggle the drawer's state with a clickable coroutine.
  • Navigation Logic: The coroutine inside clickable manages the drawer's state, opening it if closed and vice versa.
  • colors Parameter: Customizes the app bar's colors for a cohesive theme, using the TopAppBarDefaults.centerAlignedTopAppBarColors function.

Don’t forget to mention our MainScreen() in MainActivity.kt.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.codebyzebru.navigationdemo.ui.theme.NavigationDemoTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
NavigationDemoTheme {
MainScreen()
}
}
}
}

THAT’S IT!! But…

Are you not familiar with navigation components? No worries mate. I got you. Let me give you brief on, what we have done so far with navigation component any why we used it, before we end this blog here.

A Quick Basics Guide: Navigation Components

1. NavHostController:

The NavHostController is like the captain of our navigation ship. It steers the app through different screens or destinations. In simpler terms, it's the one responsible for knowing where the app is and where it should go next.

2. NavHost:

The NavHost is the stage where our navigation drama unfolds. It's the container that holds all our scenes (composables). Think of it as the theater where the show happens. It takes the NavHostController and the starting destination as its parameters.

3. composable:

Each composable is like a scene in our play. It represents a screen or a view. In our case, we have two scenes: one for the home screen and another for the about screen. When the NavHostController reaches a certain point in the navigation, it shows the corresponding composable—just like switching scenes in a play.

4. ActiveScreen Sealed Class:

The ActiveScreen sealed class is our script. It defines the different screens (Home and About) with their properties, such as icons, content descriptions, titles, and routes. Each object inside this class is like a character in our navigation story.

Putting it Together:

Imagine our app as a theater production. The NavHostController is the director guiding the actors (composables) through different scenes (composables). The ActiveScreen sealed class is our script, detailing the characters and their roles.

In our code, the DrawerNavGraph.kt file acts as a script supervisor, telling the director (NavHostController) which scenes to include and when. So, when a user interacts with the navigation drawer, it's like flipping through pages in our script, and the director ensures the right scenes are played.

We’ve successfully implemented a navigation drawer using Material Design 3 in Jetpack Compose. Congratulations on navigating through the intricacies of building a sleek and functional UI.

I hope this article has provided valuable insights and assistance for your Android journey. Your support means a lot to me, so if you found this content helpful, please start following me and don’t hesitate to show some love with a hearty round of applause!👏

Your feedback fuels my passion for creating quality content. For any Android queries or just to connect, reach out on LinkedIn and Twitter.

Thanks for reading — looking forward to staying in touch!

Happy Coding!!

--

--

Meet Patadia
Meet Patadia

Written by Meet Patadia

Software Developer - Android, Java, Kotlin, MVVM, Jetpack Compose

No responses yet