Navigation Rail with Material 3 in Jetpack Compose

Introduction

Navigation rails are predominantly present in devices with larger screens, such as tablets, where user movement through the bottom navigation is problematic. In scenarios involving expandable displays, the utilization of navigation icons situated in the sidebar simplifies the process of navigation.

Navigation Rail with Material 3

The window size class is an experimental API that introduces three distinct screen size categories: compact, medium, and expanded. When the screen size surpasses the compact category, the navigation rail becomes a viable option, whereas smaller sizes warrant the use of the bottom navigation bar.

In the context of Material 3, three primary types of navigation components exist the Navigation bar, Navigation drawer, and Navigation rail. We covered the Navigation bar and Navigation drawer in the last article. Therefore in this article, we will discuss the Navigation rail. So let us begin.

Navigation Rail Example

Implementing the Navigation Rail

Let's dive into how you can implement a navigation Rail using Material 3 in Jetpack Compose.

1. Add Dependency

Create a new project or open any project, then the following dependencies to your module build.gradle(App) file. The material3 dependency is already included in your project if you are using the most recent version of Android Studio then you can only add the window size class dependency.

implementation("androidx.compose.material3:material3")
implementation("androidx.compose.material3:material3-window-size-class:1.1.1")

2. Setting up The Navigation Rail

According to the Material 3 guidelines, the Navigation rail can contain 3 to 7 items plus a fab button. The Navigation Rail should be used in the medium and expanded window size. The Navigation should be placed in place even for different sizes and applications. 

To create the navigation rail, firstly, we must describe the content of the navigation rail, for which we will four three Navigation items (Navigation item is a custom class that we have already defined), and to preserve the state in recomposition, we will utilize the showNavigationRail state and selectedItemIndex. 

data class NavigationItem(
    val title: String,
    val unselectedIcon: ImageVector,
    val selectedIcon: ImageVector,
    val hasNews: Boolean,
    val badgeCount: Int? = null
)

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun NavigationRailExample(mainActivity: MainActivity) {

    val items = listOf(
        NavigationItem(
            title = "Location",
            selectedIcon = Icons.Default.LocationOn,
            unselectedIcon = Icons.Outlined.LocationOn,
            hasNews = false,
        ),
        NavigationItem(
            title = "Add",
            selectedIcon = Icons.Filled.AddCircle,
            unselectedIcon = Icons.Outlined.AddCircle,
            hasNews = true,
        ),
        NavigationItem(
            title = "Notification",
            selectedIcon = Icons.Filled.Notifications,
            unselectedIcon = Icons.Outlined.Notifications,
            hasNews = false,
            badgeCount = 45
        ),
        NavigationItem(
            title = "Home",
            selectedIcon = Icons.Filled.Home,
            unselectedIcon = Icons.Outlined.Home,
            hasNews = false,
        ),
    )
    val windowClass = calculateWindowSizeClass(mainActivity)
    val showNavigationRail = windowClass.widthSizeClass != WindowWidthSizeClass.Compact
    var selectedItemIndex by rememberSaveable {
        mutableStateOf(0)
    }

    Surface(
        modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background
    ) {
        Scaffold(
            bottomBar = {
                if (!showNavigationRail) {
                    // NavigationBar()
                }
            }, modifier = Modifier.fillMaxSize()
        ) { it ->
            LazyColumn(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(it)
                    .padding(
                        start = if (showNavigationRail) 80.dp else 0.dp
                    )

            ) {
                items(50) {
                    Text(
                        text = "Hello Learner $it",
                        modifier = Modifier.padding(16.dp),
                    )
                }
            }
            if (showNavigationRail) {
                // to define navigation Rail here
            }
        }
    }

}

3. Define the NavigationRail

The NavigationRail has a header and content. Content is the area of the Navigation Rail, while the header is the Navigation upper part. In the Content, we will define a Column that contains Navigation rail items, but you can define any composable here. We have four items in the column, each with text, an icon, and a badge is optional. I've defined a header in the next step to show the upper part.

@Composable
fun NavigationSideBar(
    items: List<NavigationItem>, selectedItemIndex: Int, onNavigate: (Int) -> Unit
) {
    NavigationRail(
        header = {
            // to define header here
        },
    ) {
        Column(
            modifier = Modifier.fillMaxHeight(),
            verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically)
        ) {
            items.forEachIndexed { index, item ->
                NavigationRailItem(
                    selected = selectedItemIndex == index,
                    onClick = {
                        onNavigate(index)
                    },
                    icon = {
                        NavigationIcon(
                            item = item, selected = selectedItemIndex == index
                        )
                    },
                    label = {
                        Text(text = item.title)
                    },
                )
            }
        }
    }
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NavigationIcon(
    item: NavigationItem, selected: Boolean
) {
    BadgedBox(badge = {
        if (item.badgeCount != null) {
            Badge {
                Text(text = item.badgeCount.toString())
            }
        } else if (item.hasNews) {
            Badge()
        }
    }) {
        Icon(
            imageVector = if (selected) item.selectedIcon else item.unselectedIcon,
            contentDescription = item.title
        )
    }
}

4. Define the Header

Let's continue on to creating the Navigation Rail's Header section. We encapsulate the Composable into a composable layout in this stage. We use an Icon button element for the menu icon in this layout, followed by a floating action button. The modification creates a background with an inverse surface color and moves it to the -1.dp location to differentiate it from the content.

    NavigationRail(
        header = {
            IconButton(onClick = { /*TODO*/ }) {
                Icon(
                    imageVector = Icons.Default.Menu, contentDescription = "Menu"
                )
            }
            FloatingActionButton(
                onClick = { /*TODO*/ },
                elevation = FloatingActionButtonDefaults.bottomAppBarFabElevation()
            ) {
                Icon(
                    imageVector = Icons.Outlined.Edit, contentDescription = "Edit"
                )
            }
        },
        modifier = Modifier
            .background(MaterialTheme.colorScheme.inverseOnSurface)
            .offset(x = (-1).dp)
    ) {

                // existing code 
     }

Output

Navigation Rail with Material 3 in Jetpack Compose

Summary

In this article, we see how simple it is to incorporate a Navigation Rail into a Jetpack composition. Material 3 has made modern app design more accessible, intuitive, and adaptable than ever before. Feel free to leave comments with any suggestions or corrections for this article. Thank you!


Similar Articles