Jetpack Compose Destinations
In this tutorial, you’ll learn how to implement an effective navigation pattern with Jetpack Compose, in a way that will work with different screen sizes, from phones to tablets. By Roberto Orgiu.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Jetpack Compose Destinations
20 mins
Animating All the Things
You might have noticed that if you tap again on a city, the bottom navigation bar does not disappear — which doesn’t really look that good, does it?
To fix this, you’ll add an animation to show or hide the bottom navigation bar where appropriate.
In CompatUi.kt, add this line after // TODO: Define bottomBarVisibility here
:
val bottomBarVisibility = rememberSaveable { (mutableStateOf(true)) }
This controls BottomNavigation
‘s visibility. It can show and hide the navigation based on the current screen.
Next, you need to change the visibility of the bar in each destination.
Add this code in two places: above CityListUi()
and SettingsUi()
:
LaunchedEffect(null) {
bottomBarVisibility.value = true
}
With this code, you show BottomNavigation
once the app presents the CityList screen and the Settings screen.
Now, you need to hide BottomNavigation
when presenting the city details screen. Above CityDetailUi()
, place this code:
LaunchedEffect(null) {
bottomBarVisibility.value = false
}
This code hides the bottom navigation bar.
Next, you need to change how the BottomNavigation
behaves so that it respects the visibility flag you just set up. While you’re at it, you can also animate it as it shows and hides — it’s a nice touch!
In Scaffold()
, wrap your BottomNavigation
declaration inside an AnimatedVisibility
so that it can react according to the flag. Update bottomBar
with this:
bottomBar = {
AnimatedVisibility(
// 1
visible = bottomBarVisibility.value,
// 2
enter = slideInVertically(initialOffsetY = { it }),
exit = slideOutVertically(targetOffsetY = { it }),
// 3
content = {
BottomNavigation(backgroundColor = MaterialTheme.colors.primary) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = {
Icon(
imageVector = screen.icon,
contentDescription = screen.name
)
},
label = { Text(text = screen.name) },
selected = currentDestination?.hierarchy?.any { it.route == screen.path } == true,
onClick = {
navController.navigate(screen.path) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
})
}
}
})
}
AnimatedVisibility
takes four parameters. Here’s what’s happening:
-
You use
visibility
to link the flag that you changed inside all threecomposable()
s. -
You use
enter
andexit
to regulate the enter and exit animation. -
Finally, you need the exact content that’s being animated. In this case, you use the
BottomNavigation
you created previously.
Build and run, then tap a city. You’ll see the BottomNavigation
animate in and out like this:
Refactoring for Tablets
Tablets and smartphones have pretty different kinds of space available on their screens, so you might want to change your UI from one to the other. For example, while on a phone, you could have a list and a detail on separate screens, they could be combined for a bigger tablet screen.
Another example is navigation itself. For a vertical format, you might prefer a BottomNavigation
, but with a bigger and more horizontal space you could switch to another pattern for the navigation.
Of course, you also need to know when to display each UI.
Handling Different Screen Sizes
Understanding when to display a specific UI is no easy task — and you need to find breakpoints that allow you to distinguish a phone from a tablet with reasonable certainty. Luckily, Google’s Jetpack Compose official samples have such logic in the form of the WindowSize
class. It’s included in the sample project for simplicity.
Now, you’re going to see how to use the WindowSize
class inside your app. Open MainActivity.kt and find setContent()
. In its lambda, you’ll find windowSize
. This variable contains the size of your current screen, which you’ll need to use in order to show the compact UI for phones or the extended UI for tablets.
Open MainUi.kt and wrap CompatUi()
within an if
statement:
if (windowSize == WindowSize.Compact) {
CompatUi(navController = navController, viewModel = viewModel, themeStore = themeStore)
} else {
ExpandedUi(
navController = navController,
viewModel = viewModel,
themeStore = themeStore
)
}
This snippet checks the windowSize
value in order to show either the phone version of the UI or the more expanded tablet version.
Use your tablet device or emulator to build and run the app. It doesn’t really show much.
Next, you’ll make the app show content on the tablet.
Morphing the Main Screen
You’re going to display list and detail side by side, so you won’t be navigating from one destination to another in this case. Instead, you’ll change the data on the right part of the UI based on what the user taps on the left side. This is the first big change from phone to tablet. While you can reuse your composable()
by wrapping them inside containers, you need to also adjust your logic so that it works for both situations.
Open ExpandedUi.kt. Right after // TODO: Add NavHost for tablets here
, add this code:
NavHost(navController = navController, startDestination = Screen.List.path) {
composable(Screen.List.path) {
CityListWithDetailUi(viewModel = viewModel)
}
composable(Screen.Settings.path) {
SettingsUi(themeStore = themeStore)
}
}
It’s pretty similar to what you did earlier for the phone layout, but it lacks the detail declaration. You won’t be traveling through destinations on tablets. Instead you’ll react on the list item tapped by updating a variable that will trigger a recomposition in the detail section.
To see it in detail, open CityListWithDetailUi.kt and check the beginning of the parent composable function. You’ll notice a selectedCity
mutable state. Every time you tap an item on the list, this value is updated with the selected city. Every time the selected city changes, a recomposition will occur, and CityDetailUi()
will display the new data. Fancy, isn’t it?
Now, build and run your app on a tablet, and select a city.
Reaching for the Settings Again
Once again, there’s no way to get into the settings screen. Time to fix that!
Since you have quite a lot of real estate in the horizontal axis, it makes sense to move the navigation UI there, rather than keeping it at the bottom. To do so, you’re going to use a different component — a NavigationRail. The idea is the same as in the BottomNavigation
, but this time the component will lay items down vertically, and it will stand on a side.
In ExpandedUi.kt, move the NavHost
declaration inside the Row
at the portion marked // TODO: Move NavHost here
.
Next, add this code at // TODO: Add NavigationRail here
:
NavigationRail(backgroundColor = MaterialTheme.colors.primary) {
// 1
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
// 2
items.forEachIndexed { index, screen ->
NavigationRailItem(
// 3
icon = {
Icon(
imageVector = screen.icon,
contentDescription = screen.name
)
},
label = { Text(screen.name) },
// 4
selected = currentDestination?.route?.let { it == screen.path } ?: false,
// 5
onClick = {
navController.navigate(screen.path) {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}
)
}
}
Here’s a recap of how this code works:
- You’re getting the current location on the navigation tree.
- You cycle through all the available destinations.
- For each item, you load its icon and its label.
- You mark the item as selected if the currently presented destination matches the item itself.
- You define what happens when users tap the icon.
This code is really very similar to the code you wrote a few minutes ago for the BottomNavigation
. The only clear differences are in the NavigationRail
and NavigationRailItem
declarations.
Build and run on a tablet. Look at your amazing side navigation!