November 4th, 2021

Jetpack Compose Navigation Rail

Kristen Halper
SW/FW Engineer

Hello Jetpack Compose developers!

This week, we released a new sample that demonstrates how to use the NavigationRail component with large screen and foldable devices. These guidelines were also introduced recently in the Microsoft Surface Duo Design Kit 2.1.

Create a NavigationRail with Compose

We first blogged about NavigationRail in August, when we described how to use it in a dual-screen sample with XML layouts. As a reminder, the NavigationRail component is used for navigation on large screens and can hold 3-7 app destinations, plus a logo or floating action button.

To add a NavigationRail to your Compose application, follow these steps:

  1. Add the following dependencies to your build.gradle file:
    ext {
       composeVersion = "1.1.0-alpha06"
       navVersion = "2.4.0-alpha10"
    }
    implementation "androidx.compose.material:material:$composeVersion"
    implementation "androidx.navigation:navigation-compose:$navVersion"

    The component is still in alpha, so make sure to update composeVersion and navVersion with the most recent releases, which can be found at the Compose Material and Navigation pages. You will also have to add the @ExperimentalMaterialApi tag to any Composable functions that use NavigationRail or NavigationRailItem.

  2. Create a composable with a NavigationRail that contains NavigationRailItem components. The code snippet below is based on an example from the NavigationRail API reference, but similar code can be found in our sample in GalleryNavWithSelector.kt.
    @ExperimentalMaterialApi
    @Composable
    fun ExampleNavRail(navController: NavHostController, items: List) {
       val icons = listOf(Icons.Filled.Home, Icons.Filled.Search, Icons.Filled.Settings)
    
       NavigationRail {
           items.forEachIndexed { index, item ->
               val currentDestination = navController.currentBackStackEntryAsState().value?.destination
               NavigationRailItem(
                   icon = { Icon(icons[index], contentDescription = item) },
                   label = { Text(item) },
                   selected = currentDestination?.route?.let { it == item } ?: false,
                   onClick = { navController.navigate(item) }
               )
           }
       }
    }
  3. Add your NavigationRail composable next to your NavHost by placing them together in a Row.
    val navController = rememberNavController()
    val items = listOf("Home", "Search", "Settings")
    
    Scaffold(topBar = { TopAppBar(title = { Text("Example Nav Rail App") }) }) {
       Row {
           ExampleNavRail(navController, items)
           NavHost(navController = navController, startDestination = items.first()) {
               items.forEach { item ->
                   composable(route = item) { Text(text = item) }
               }
           }
       }
    }

Design guidelines for large screen and foldable devices UX

When adding navigation to apps for large screen and foldable devices, we recommend using a combination of NavigationRail and BottomNavigation. In the case of Surface Duo, this means using a navigation rail when the app is spanned and switching to a bottom navigation bar when the app is unspanned.

Resource from Surface Duo Design Kit 2.1 that shows options for NavigationRail and BottomNavigation

Figure 1: Navigation rail guidelines from the Surface Duo Design Kit 2.1.

To dynamically switch between navigation components based on screen size, use a combination of the Jetpack Window Manager library and other device properties available via LocalConfiguration.current. On Surface Duo and other foldables, you can use Window Manager to check if a hinge is present in the form of a FoldingFeature, while LocalConfiguration.current is useful for checking the size of a device’s screen. To see how we used these two resources in the sample app, check out the SetupUI function in HomePage.kt.

Sample highlights and challenges

Our NavigationRail sample is a nature information catalog with five categories: plants, birds, animals, lakes, and rocks. Each category has multiple items, and each item has a picture and a scientific description. The app uses our custom control TwoPaneLayout and follows the List Detail design pattern, with the “list” being the gallery for the current category and the “detail” being the picture and description of the selected item.

Navigation hierarchy

In our plans for the sample, we decided on the layout designs shown in Figure 2. When it came time to implement the behavior, setting up the navigation was a bit complicated!

Layout view design diagram, showing the placement for the NavigationRail / BottomNavigation, detail view for selected item, and list view for selected gallery;Dual portrait mode: navigation rail on left, list view on left, detail view on right;Dual landscape mode: navigation rail on left, list view or detail view on top and bottom (can navigate between); Single portrait/landscape modes: bottom navigation on bottom, list view or detail view in the rest of the screen (can navigate between) Figure 2: Layout designs for each Surface Duo orientation.

After some thought, we realized that the BottomNavigation/NavigationRail would always be shown in pane 1, and the associated NavHost would only be used to display the different galleries. Even though we would sometimes need to switch between the gallery and item views, it wouldn’t make sense to add the item view to the NavHost as a top-level app destination.

So, the final navigation graph we settled on has two main parts. The first part, as shown in Figure 3, uses the navigation support provided by TwoPaneLayout to switch between the gallery and item views when only one pane is being displayed.

TwoPaneLayout navigation diagram; Pane 1 holds navigation rail/bottom navigation and NavHost, Pane 2 holds ItemDetailView, can use navigateToPane2 and navigateToPane1 methods to switch between; Logic flow: if dual portrait, show 2 panes; if not, check if item is selected. If item is selected, show ItemDetailView via navigateToPane2; if not, show NavHost/GalleryView via navigateToPane1 Figure 3: Navigation logic within TwoPaneLayout.

The second part, seen in Figure 4, exists only within pane 1, where a NavHost is connected to the current navigation component to enable switching between the five different gallery categories. One important note is that because the NavHost only exists at the pane 1 level in the layout hierarchy, the app technically had two NavHost instance; one for the single pane container and another for the two pane container. To ensure that both instances were synced, we created a currentRoute variable at the highest level of the hierarchy and checked it when recomposing pane 1.

NavHost navigation logic diagram; Inside Pane 1, with either navigation rail or bottom navigation, then NavHost/GalleryView in the rest of the pane; NavHost/GalleryView has 5 different appearance options for the 5 different categories: plants, birds, animals, lakes, and rocks Figure 4: Navigation logic within pane 1 with a NavHost.

Custom components

Another challenge was creating custom components to match the designs for the sample. We found that the Jetsnack sample from the official Compose repository was a great reference for both setting up navigation and adding custom UI components.

The first customization we made was adding a selector to the NavigationRailItem and BottomNavigationItem components. This selector appears behind the icon of the current destination and includes visibility animations (see Figure 5). If you’re interested in the implementation, check out NavRailItemWithSelector.kt and BottomNavItemWithSelector.kt.

Animation of Jetpack Componse Navigation Rail on Surface Duo 2 Figure 5: Example of how the selector highlights the current destination in the NavigationRail.

We also created a custom ContentDrawer component that behaves similarly to a BottomSheetScaffold, except with an enhancement that prevents content from rendering under the hinge.

Animation of ContentDrawer on Surface Duo 2 Figure 6: Demonstration of hinge aware ContentDrawer component.

This was achieved by creating a swipeable BoxWithConstraints and adding a Spacer with dynamic height based on swipe progress. The code snippet below was taken from ContentDrawer.kt, and it shows how height was calculated using hinge properties and information from the swipeable modifier.

// Check if a spacer needs to be included to render content around an occluding hinge
val spacerHeight = if (hingeOccludes) {
   val isExpanding = swipeableState.progress.to == DrawerState.Expanded
   val progressHeight = (hingeSize.value * swipeableState.progress.fraction).dp
   if (isExpanding)
       progressHeight
   else
       hingeSize - progressHeight
} else {
   0.dp
}

Resources and feedback

For coding tips, visit the Surface Duo developer documentation , our samples, and Google’s Android large screen development guidance.

If you have any questions, or would like to tell us about your dual-screen apps, use the feedback forum or message us on Twitter @surfaceduodev.

Finally, please join us for our dual screen developer livestream at 11am (Pacific time) each Friday – mark it in your calendar and check out the archives on YouTube.

Attribution

The images used in the Navigation rail sample are licensed under [© eakarat], [© nerudol], [© DedMityay], [© tibor13], [© Harry Collins], [© Tony Baggett], [© WildMedia], [© Vlasto Opatovsky], [© Lakeview Images], [© viktoriya89], [© dudlajzov], [© zkbld], [© muraviov], [© phototrip.cz], [© gpisano71], [© Mike Mareen], [© Fokke], [© ronnybas], [© Piotr Krzeslak], [© shaiith], [© aiisha], [© Andrew Mayovskyy], [© Andrey Armyagov], [© Наталья Босяк] / stock.adobe.com

Author

Kristen Halper
SW/FW Engineer

Works in the Surface Duo Developer Experience team to help with all aspects of dual-screen SDK development and customer engagement.

0 comments

Discussion are closed.