Adaptive Navigation Suite in Jetpack Compose
Throughout the years, Android devices expanded in multiple form factors and multiple screen sizes. In order to handle this diversity, Google has introduced mechanisms to adapt the UI. Recently, in Google I/O 2024, Google introduced the Adaptive Navigation Suite in Jetpack Compose. This suite provides a set of tools to adapt the navigation UI based on the screen size and form factor. In this article, we will explore the Adaptive Navigation Suite in Jetpack Compose and how it can be used to create an adaptive navigation UI.
The old way
In the past, developers had to manually handle the adaptation of the navigation UI based on the screen sizes. This involved writing a lot of boilerplate code to check the screen size and form factor and then decide which navigation UI to show. This approach was not only time-consuming but also error-prone, as developers had to handle different edge cases for various devices.
For instance, lets say you want to show a bottom bar on smaller screens and a navigation rail or navigation drawer on larger screens. One of the ways to implement such a behavior is to use `BoxWithConstraints` composable and determine the breakpoints based on the screen size. This approach requires a lot of manual calculations and is not very efficient. It may look like the following:
BoxWithConstraints(
modifier = Modifier.fillMaxSize()
) {
val minWidth = constraints.minWidth.dp
val minHeight = constraints.minHeight.dp
if (minWidth >= 600.dp || minHeight >= 900.dp) {
// Show navigation rail or navigation drawer
} else {
// Show bottom bar
}
}
(To learn more about “breakpoints” or Window size classes, check out the official documentation)
And then you have to manually handle the navigation UI based on the screen size, implement bottom bar for smaller screens and navigation rail or navigation drawer for larger screens. This approach is not only cumbersome but also not very scalable. This is why Google introduced the Adaptive Navigation Suite in Jetpack Compose.
Navigation Suite
The main goal of adaptive navigation suite is to provide a way to adapt the navigation UI based on the screen size and form factor. To implement the adaptive navigation, we need to use `NavigationSuiteScaffold` composable and rest is just magic. The `NavigationSuiteScaffold` then takes care of the rest. It changes the navigation UI based on the screen size and form factor. For example, it shows a bottom bar on smaller screens and a navigation rail or navigation drawer on larger screens.
First, add the following dependencies to your project
[version]
material3-adaptive-navigation-suite = "1.3.0-beta02"
...
[libraries]
androidx-compose-material3-adaptive-navigation-suite = { module = "androidx.compose.material3:material3-adaptive-navigation-suite", version.ref = "material3-adaptive-navigation-suite" }
then apply the dependencies to the project’s build.gradle file.
dependencies {
implementation(libs.androidx.compose.material3.adaptive.navigation.suite)
}
After adding the dependencies, we can implement the adaptive navigation suite.
val selectedItem = remember { mutableStateOf(Destination.Account) }
NavigationSuiteScaffold(navigationSuiteItems = {
Destination.all.forEach {
item(
selected = selectedItem.value == it,
onClick = { selectedItem.value = it },
label = { Text(it.label) },
icon = {
Icon(
imageVector = it.icon,
contentDescription = it.contentDescription
)
},
alwaysShowLabel = true,
)
}
}) {
Box(
modifier = Modifier.fillMaxSize()
) {
Surface(
modifier = Modifier
.align(Alignment.Center)
) {
when (selectedItem.value) {
Destination.Account -> Text("Account")
Destination.Search -> Text("Search")
}
}
}
}
enum class Destination(
val label: String, val icon: ImageVector, val contentDescription: String? = null
) {
Account("Account", Icons.Filled.AccountCircle), Search("Search", Icons.Filled.Search);
companion object {
val all = values()
}
}
And the output of the code will be like this:
Just by these few lines of code, we have adaptive navigation UI that changes based on the screen size and form factor. By updating `selectedItem` value, we are able to change the content of the screen too.
Combining with `NavHost`
Furthermore, the `NavigationSuiteScaffold` composable is not here to replace “NavHost”, since it is just a way to adapt the screen changes based on the screen size and form factor. The `NavHost` is still used to navigate between different screens. In fact, I believe that it is a good practice to use `NavHost` inside the `NavigationSuiteScaffold` to navigate between different screens. So that the `NavigationSuiteScaffold` will take care of the navigation UI and `NavHost` will take care of the screen changes and this will make migration to `NavigationSuiteScaffold` easier.
Integration of these two composable will look like this:
@Composable
fun MainScreen() {
val navHost = rememberNavController()
val navBackStackEntry by navHost.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
NavigationSuiteScaffold(
modifier = Modifier.fillMaxSize(),
navigationSuiteItems = navigationSuiteItems(currentDestination, navHost)
) {
NavHost(
navController = navHost,
startDestination = "screen1"
) {
composable(
route = "screen1"
) {
Screen1()
}
composable(
route = "screen2"
) {
Screen2(
onDetailClicked = {
navHost.navigate("screen3")
}
)
}
composable(
route = "screen3"
) {
Screen3()
}
}
}
}
@Composable
private fun navigationSuiteItems(
currentDestination: NavDestination?,
navHost: NavHostController
): NavigationSuiteScope.() -> Unit = {
item(
selected = currentDestination?.hierarchy?.any { it.route == "screen1" } == true,
onClick = {
navigateWithBackStackHandling("screen1", navHost)
},
label = { Text("Screen 1") },
icon = {
Icon(
imageVector = MainScreen.AccountScreen.icon, contentDescription = "Screen 1"
)
},
)
item(
selected = currentDestination?.hierarchy?.any { it.route == "screen2" } == true,
onClick = {
navigateWithBackStackHandling("screen2", navHost)
},
label = { Text("Screen 2") },
icon = {
Icon(
imageVector = MainScreen.AccountScreen.icon, contentDescription = "Screen 2"
)
},
)
}
fun navigateWithBackStackHandling(route: String, navHost: NavHostController) {
navHost.navigate(route) {
popUpTo(navHost.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
(To learn why `navigateWithBackStackHandling` is implemented this way, check out the official documentation)
After integrating `NavHost` with `NavigationSuiteScaffold`, we have adaptive navigation UI that changes based on the screen size and form factor and also we are able to navigate between different screens using `NavHost`.
Conclusion
In conclusion, the Adaptive Navigation Suite in Jetpack Compose provides a powerful way to adapt the navigation UI based on the screen size and form factor. By using the `NavigationSuiteScaffold` composable, developers can easily create adaptive navigation UI that changes based on the screen size. This not only simplifies the implementation but also ensures a consistent user experience across different devices. By integrating `NavHost` with `NavigationSuiteScaffold`, developers can navigate between different screens while maintaining the adaptive navigation UI. Overall, the Adaptive Navigation Suite in Jetpack Compose is a valuable tool for building adaptive Android apps that cater to a diverse range of devices.
References
Official documentation:
Sample repository: