In this article, I'll guide you through integrating a Bottom Navigation Bar for compact screens and a Navigation Rail for larger screens in a Compose Multiplatform app. By the end, you'll have a fully functional app that adapts to various screen sizes, and can be deployed on Android, iOS, macOS, Linux, and Windows.
Step 1: Add Dependencies to
libs.versions.toml
First, add the necessary dependencies for navigation and window size classes in your libs.versions.toml file. This file defines the versions and library modules you'll use in your project:
[versions]
# Define the versions of the dependencies
navigationCompose = "2.8.0-alpha02"
size = "0.5.0"
[libraries]
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigationCompose" }
screen-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "size" }Next, include these dependencies in the build.gradle.kts file of your Compose Multiplatform project to make them available across platforms:
// build.gradle.kts (Module :composeApp)
kotlin {
sourceSets {
val commonMain by getting {
dependencies {
// Material 3
implementation(compose.material3)
// Navigation
implementation(libs.navigation.compose)
// window-size
implementation(libs.screen.size)
}
}
}
}Step 2: Create a
NavigationItemData Class
To manage the items for both the bottom navigation and the navigation rail, we need a data class that defines the properties for each navigation item. Here's how you can create a NavigationItem data class that works across different platforms in Compose:
import androidx.compose.ui.graphics.vector.ImageVector
import org.jetbrains.compose.resources.DrawableResource
import org.jetbrains.compose.resources.StringResource
data class NavigationItem(
val unSelectedIcon: ImageVector /* or DrawableResource*/,
val selectedIcon: ImageVector /* or DrawableResource*/,
val title: String /* or StringResource */,
val route: String
)In this step, we:
- create a
NavigationItemdata class.
This data class allows you to define the icons, titles, and routes for each item in both the bottom navigation bar and navigation rail. You can use ImageVector or DrawableResource for the icons, and String or StringResource for the titles, making it flexible for different resource types in Compose Multiplatform.
Step 3: Define Navigation Routes and Items
Next, you'll want to define the navigation routes for your app and list the navigation items. Here's how to set up a simple route system using sealed class and a list of NavigationItem objects:
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
object Graph {
const val NAVIGATION_BAR_SCREEN_GRAPH = "navigationBarScreenGraph"
}
sealed class Routes(var route: String) {
data object Home : Routes("home")
data object Setting : Routes("setting")
data object HomeDetail : Routes("homeDetail")
data object SettingDetail : Routes("settingDetail")
}
val navigationItemsLists = listOf(
NavigationItem(
unSelectedIcon = Icons.Outlined.Home,
selectedIcon = Icons.Filled.Home,
title = "Home",
route = Routes.Home.route,
),
NavigationItem(
unSelectedIcon = Icons.Outlined.Search,
selectedIcon = Icons.Filled.Search,
title = "Setting",
route = Routes.Setting.route,
),
)In this step, we:
- Defined the main routes in the app using a sealed class
Routes. - Created a list of
NavigationItemobjects, specifying the icons, titles, and routes for each item. These items will be used in both the bottom navigation bar and the navigation rail to handle navigation based on screen size.
Step 4: Implement the Bottom Navigation Bar and Navigation Rail
To handle navigation across different screen sizes, we'll implement both a Bottom Navigation Bar (for smaller screens) and a Navigation Rail (for larger screens). These components will dynamically display navigation items based on the current route.
Here's how you can set up the BottomNavigationBar and NavigationSideBar composables:
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@Composable
fun BottomNavigationBar(
items: List<NavigationItem>,
currentRoute: String?,
onItemClick: (NavigationItem) -> Unit
) {
NavigationBar(
modifier = Modifier.fillMaxWidth(),
) {
items.forEach { navigationItem ->
NavigationBarItem(
selected = currentRoute == navigationItem.route,
onClick = { onItemClick(navigationItem) },
icon = {
Icon(
imageVector = if (navigationItem.route == currentRoute) navigationItem.selectedIcon else navigationItem.unSelectedIcon,
contentDescription = navigationItem.title,
)
},
label = {
Text(
text = navigationItem.title,
style = if (navigationItem.route == currentRoute) MaterialTheme.typography.labelLarge
else MaterialTheme.typography.labelMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
)
}
}
}
@Composable
fun NavigationSideBar(
items: List<NavigationItem>,
currentRoute: String?,
onItemClick: (NavigationItem) -> Unit
) {
NavigationRail(
modifier = Modifier.fillMaxHeight(),
containerColor = MaterialTheme.colorScheme.surface,
) {
items.forEach { navigationItem ->
NavigationRailItem(
selected = navigationItem.route == currentRoute,
onClick = { onItemClick(navigationItem) },
icon = {
Icon(
imageVector = if (navigationItem.route == currentRoute) navigationItem.selectedIcon else navigationItem.unSelectedIcon,
contentDescription = navigationItem.title,
)
},
modifier = Modifier.padding(vertical = 12.dp),
label = {
Text(
text = navigationItem.title,
style = if (navigationItem.route == currentRoute) MaterialTheme.typography.labelLarge
else MaterialTheme.typography.labelMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
)
}
}
}In this step, we:
- Implemented the
BottomNavigationBarcomposable for small screens. It uses aNavigationBarto display the items and highlights the selected one based on thecurrentRoute. - Created the
NavigationSideBarcomposable for large screens. It uses aNavigationRailand similarly highlights the selected item.
Both components are designed to switch between the unselected and selected icons, update the text style based on the route, and handle clicks with the onItemClick callback.
Step 5: Implement Home and Home Detail Screens
To create the screens that will be displayed when navigating between the home and its detail, we define HomeScreen and HomeDetailScreen. These screens handle user input and navigation between each other.
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.meet.bottom_navigation_bar_navigation_rail.navigation.Routes
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
rootNavController: NavController, paddingValues: PaddingValues
) {
var name by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
TopAppBar(
title = {
Text(
text = "Home",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
}
)
Spacer(modifier = Modifier.height(20.dp))
TextField(
value = name,
onValueChange = { name = it },
label = { Text(text = "Enter the name") }
)
Button(onClick = {
rootNavController.currentBackStackEntry?.savedStateHandle?.apply {
set("name", name)
}
rootNavController.navigate(Routes.HomeDetail.route)
}) {
Text(
text = "Move to Home Detail Screen",
fontSize = 20.sp
)
}
Spacer(modifier = Modifier.height(20.dp))
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeDetailScreen(
rootNavController: NavController,
name: String
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
TopAppBar(
title = {
Text(
text = "Home Detail",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
},
navigationIcon = {
IconButton(onClick = { rootNavController.navigateUp() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
Text(
text = "Name = $name",
fontSize = 20.sp
)
Spacer(modifier = Modifier.height(20.dp))
}
}In this step:
HomeScreenallows users to input a name and navigate toHomeDetailScreen, passing the entered name usingsavedStateHandle.HomeDetailScreendisplays the passed name and provides a back button to return toHomeScreen.
Step 6: Implement Setting and Setting Detail Screens
Now, let's create the SettingScreen and SettingDetailScreen to manage settings in your app and allow navigation between these screens.
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.meet.bottom_navigation_bar_navigation_rail.navigation.Routes
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingScreen(
rootNavController: NavController,
paddingValues: PaddingValues
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
TopAppBar(
title = {
Text(
text = "Setting",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
}
)
Spacer(modifier = Modifier.height(20.dp))
Button(onClick = {
rootNavController.navigate(Routes.SettingDetail.route)
}) {
Text(
text = "Move to Setting Detail Screen",
fontSize = 20.sp
)
}
Spacer(modifier = Modifier.height(20.dp))
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingDetailScreen(
rootNavController: NavController,
) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.CenterHorizontally
) {
TopAppBar(
title = {
Text(
text = "Setting Detail",
style = MaterialTheme.typography.headlineLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onBackground
)
},
navigationIcon = {
IconButton(onClick = { rootNavController.navigateUp() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
}
}In this step:
SettingScreenpresents a button to navigate toSettingDetailScreen.SettingDetailScreenprovides a back button to return to theSettingScreen.
Step 7: Set Up the Navigation Graph
To handle screen navigation in your Compose Multiplatform app, set up the navigation graph. This will include both the main screens and detail screens. The RootNavGraph function will define the main structure for navigation, while the mainNavGraph function will handle the individual navigation routes.
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.meet.bottom_navigation_bar_navigation_rail.navigation.Graph
import com.meet.bottom_navigation_bar_navigation_rail.navigation.Routes
import com.meet.bottom_navigation_bar_navigation_rail.screens.HomeDetailScreen
import com.meet.bottom_navigation_bar_navigation_rail.screens.HomeScreen
import com.meet.bottom_navigation_bar_navigation_rail.screens.SettingDetailScreen
import com.meet.bottom_navigation_bar_navigation_rail.screens.SettingScreen
@Composable
fun RootNavGraph(
rootNavController: NavHostController,
innerPadding: PaddingValues
) {
NavHost(
navController = rootNavController,
startDestination = Graph.NAVIGATION_BAR_SCREEN_GRAPH,
) {
mainNavGraph(rootNavController = rootNavController, innerPadding = innerPadding)
composable(
route = Routes.HomeDetail.route,
) {
rootNavController.previousBackStackEntry?.savedStateHandle?.get<String>("name")?.let { name ->
HomeDetailScreen(rootNavController = rootNavController, name = name)
}
}
composable(
route = Routes.SettingDetail.route,
) {
SettingDetailScreen(rootNavController = rootNavController)
}
}
}
fun NavGraphBuilder.mainNavGraph(
rootNavController: NavHostController,
innerPadding: PaddingValues
) {
navigation(
startDestination = Routes.Home.route,
route = Graph.NAVIGATION_BAR_SCREEN_GRAPH
) {
composable(route = Routes.Home.route) {
HomeScreen(rootNavController = rootNavController, paddingValues = innerPadding)
}
composable(route = Routes.Setting.route) {
SettingScreen(rootNavController = rootNavController, paddingValues = innerPadding)
}
}
}In this step:
- We define a
RootNavGraphthat starts the navigation graph and handles both main and detail screens. mainNavGraphmanages the navigation for theHomeScreenandSettingScreen.- We use
composableto declare each screen's route, and for the detail screens (HomeDetailScreen,SettingDetailScreen), we retrieve parameters usingsavedStateHandlewhen navigating between them.
Step 8: Building the Adaptive UI for Different Screen Sizes
In this step, we'll implement the MainScreen composable, which handles displaying either the Bottom Navigation Bar or the Navigation Rail based on the screen size.
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.runtime.*
import androidx.navigation.NavHostController
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.meet.bottom_navigation_bar_navigation_rail.navigation.*
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Composable
fun MainScreen() {
val windowSizeClass = calculateWindowSizeClass()
val isMediumExpandedWWSC by remember(windowSizeClass) {
derivedStateOf {
windowSizeClass.widthSizeClass != WindowWidthSizeClass.Compact
}
}
val rootNavController = rememberNavController()
val navBackStackEntry by rootNavController.currentBackStackEntryAsState()
val currentRoute by remember(navBackStackEntry) {
derivedStateOf {
navBackStackEntry?.destination?.route
}
}
val navigationItem by remember {
derivedStateOf {
navigationItemsLists.find { it.route == currentRoute }
}
}
val isMainScreenVisible by remember(isMediumExpandedWWSC) {
derivedStateOf {
navigationItem != null
}
}
val isBottomBarVisible by remember(isMediumExpandedWWSC) {
derivedStateOf {
if (!isMediumExpandedWWSC) {
navigationItem != null
} else {
false
}
}
}
MainScaffold(
rootNavController = rootNavController,
currentRoute = currentRoute,
isMediumExpandedWWSC = isMediumExpandedWWSC,
isBottomBarVisible = isBottomBarVisible,
isMainScreenVisible = isMainScreenVisible,
onItemClick = { currentNavigationItem ->
rootNavController.navigate(currentNavigationItem.route) {
popUpTo(rootNavController.graph.startDestinationRoute ?: "") {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
@Composable
fun MainScaffold(
rootNavController: NavHostController,
currentRoute: String?,
isMediumExpandedWWSC: Boolean,
isBottomBarVisible: Boolean,
isMainScreenVisible: Boolean,
onItemClick: (NavigationItem) -> Unit,
) {
Row {
AnimatedVisibility(
modifier = Modifier.background(MaterialTheme.colorScheme.surface),
visible = isMediumExpandedWWSC && isMainScreenVisible,
enter = slideInHorizontally(
// Slide in from the left
initialOffsetX = { fullWidth -> -fullWidth }
),
exit = slideOutHorizontally(
// Slide out to the right
targetOffsetX = { fullWidth -> -fullWidth }
)
) {
NavigationSideBar(
items = navigationItemsLists,
currentRoute = currentRoute,
onItemClick = { currentNavigationItem ->
onItemClick(currentNavigationItem)
}
)
}
Scaffold(
bottomBar = {
AnimatedVisibility(
visible = isBottomBarVisible,
enter = slideInVertically(
// Slide in from the bottom
initialOffsetY = { fullHeight -> fullHeight }
),
exit = slideOutVertically(
// Slide out to the bottom
targetOffsetY = { fullHeight -> fullHeight }
)
) {
BottomNavigationBar(
items = navigationItemsLists,
currentRoute = currentRoute,
onItemClick = { currentNavigationItem ->
onItemClick(currentNavigationItem)
}
)
}
}
) { innerPadding ->
RootNavGraph(
rootNavController = rootNavController,
innerPadding = innerPadding,
)
}
}
}Here, we use WindowSizeClass to dynamically adapt the UI based on the width of the device screen. If the width is compact, we show the Bottom Navigation Bar; if it's larger (like on tablets or desktops), we switch to a Navigation Rail for better usability.
Here's a breakdown of how it works:
WindowSizeClass: Helps detect the screen size. We use it to differentiate between compact and expanded layouts.AnimatedVisibility: This Jetpack Compose API is used to animate the transition between the Bottom Navigation Bar and the Navigation Rail. For example, when switching to a larger screen size, the Navigation Rail slides in from the left, giving a polished, responsive look.Scaffold: Acts as the container for the UI layout, managing the positioning of the Bottom Navigation Bar and the Navigation Rail, along with handling content padding.
By doing this, you create a flexible UI that works for both smaller devices like phones, where space is limited, and larger screens like tablets and desktops, where more space allows for a side navigation rail.
Step 9: Create the Main Composable Function
In this step, we'll define the App composable in the common module. This function will act as the entry point of the application, applying a MaterialTheme to the MainScreen.
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
MaterialTheme {
MainScreen()
}
}In this step:
- We define the
Appcomposable, which wrapsMainScreenin aMaterialTheme, ensuring a consistent look and feel across the app. - A
@Previewannotation is used for quickly visualizing theAppcomposable during development.
Step 10: Set Up the Desktop Module
Now let's set up the main function in the desktop module to launch the app. This function defines the application window and the entry point of the desktop app.
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import java.awt.Dimension
fun main() = application {
Window(
onCloseRequest = ::exitApplication,
title = "Bottom-Navigation-Bar-Navigation-Rail",
) {
window.minimumSize = Dimension(640, 480)
App()
}
}In this step:
- We use
Windowfromandroidx.compose.ui.windowto create the desktop app's main window. App()is called to display the UI within the window.- The minimum window size is set to
640x480to ensure proper display on smaller screens.
Conclusion
You've successfully integrated Bottom Navigation Bar for compact screens and a Navigation Rail for larger screens in a Compose Multiplatform. Now, you can run the app on each platform — Android, iOS, macOS, Linux, and Windows — to ensure everything works smoothly.
Example Github Repo:
If you're interested in learning more about Kotlin Multiplatform and Compose Multiplatform, check out my playlist on YouTube Channel: Mastering Kotlin Multiplatform with Jetpack Compose: Complete Guide in Hindi
Thank you for reading! 🙌🙏✌ I hope you found this guide useful.
Don't forget to clap 👏 to support me and follow for more insightful articles about Android Development, Kotlin, and KMP. If you need any help related to Android, Kotlin, and KMP, I'm always happy to assist.
Explore More Projects
If you're interested in seeing full applications built with Kotlin Multiplatform and Jetpack Compose, check out these open-source projects:
- News Kotlin Multiplatform App (Supports Android, iOS, Windows, macOS, Linux): News KMP App is a Kotlin Compose Multiplatform (KMP) project that aims to provide a consistent news reading experience across multiple platforms, including Android, iOS, Windows, macOS, and Linux. This project leverages Kotlin's multiplatform capabilities to share code and logic while using Compose for UI, ensuring a seamless and native experience on each platform. GitHub Repository: News-KMP-App
- Gemini AI Kotlin Multiplatform App (Supports Android, iOS, Windows, macOS, Linux, and Web): Gemini AI KMP App is a Kotlin Compose Multiplatform project designed by Gemini AI where you can retrieve information from text and images in a conversational format. Additionally, it allows storing chats group-wise using SQLDelight and KStore, and facilitates changing the Gemini API key. GitHub Repository: Gemini-AI-KMP-App
Follow me on
Medium , YouTube , GitHub , Instagram , LinkedIn , Buy Me a Coffee , Twitter