Technologies Natives

Can I use Google Accompanist Navigation-material to navigate between multiple compose bottom sheets?

Introduction

Introducing bottom sheet and navigation in compose

Compose gives Android developers a new way to implement UI. If you still hesitate to use it, you should read this article and be convinced. But Compose is not always so easy to use, let’s see it!

You want to use a bottom sheet with compose and be able to navigate from a bottom sheet to another with the Android back button managed like your sheet is a screen? It’s possible, there are several ways to do it. 

First, let’s talk about the bottom sheet itself without combining it with navigation. With compose, there are 3 manners to implement it:

  • Use ++code>ModalBottomSheetLayout++/code>
  • Use ++code>BottomSheetScaffold ++/code>
  • Use your own implementation
    - With a ++code>DialogFragment++/code>
    - With an entire compose view from scratch

Now, let’s combine a bottom sheet with the navigation. Thanks to Google Accompanist, we have a simple solution. Their solution is not perfect, but we found some tricky hacks to do exactly what we want.

But what do we exactly want? A picture is worth a thousand words:

This scheme represents what we want the most. Blue represents what doesn’t currently exist in the Google Accompanist library. Also, it’s not possible to skip the half extended position of the bottom sheet using the accompanist default implementation. As a bonus, it should be good to navigate to a second screen or to go to the bottom sheet “X B” from “Y A” (spoiler: it’s included in the library).

Compose and other Android library dependencies

Use the compose library, for example use 1.2.0 version which is compatible with Kotlin 1.7.0 

++pre> ++code> implementation "androidx.compose.ui:ui:$compose_version" implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" // bottomSheet implementation "androidx.compose.material:material:$compose_version" //navigation implementation "androidx.navigation:navigation-compose:2.5.1" //accompanist implementation "com.google.accompanist:accompanist-navigation-material:0.25.0" ++/pre> ++/code>

Android Accompanist Navigation Material library

Key points of the library

This library works with two key points.

  1. The first is that it uses the ++code>ModalBottomSheetLayout++/code> and wraps it with a ++code>BottomSheetNavigator++/code>. This navigator is a normal navigator from ++code>androidx.navigation++/code> that can manage the bottomSheetState (to show and hide bottom sheet) and navigate between the multiple bottom sheets.
  2. The second key point is the Kotlin extension of compose ++code>NavGraphBuilder++/code> to specify a second kind of destination: ++code>bottomSheet++/code>. That way, in the ++code>NavHost++/code> where you specify the compose destination, the library has an indication to tell if you want to open a bottom sheet or just navigate to another full screen composable.

Another advantage of using this library is that it already fixes some bugs linked to the bottom sheet itself. For example, the fact that a bottom sheet with 0dp height will crash. So your composable now looks like this:

++pre>++code> ModalBottomSheetLayout( bottomSheetNavigator = bottomSheetNavigator, sheetShape = BottomSheetShape() ) { NavHost(navController = navController, startDestination = Destinations.Home) { composable(route = Destinations.Screen1) { HomeScreenContent() } bottomSheet(route = Destinations.Sheet_X_A) { BottomSheetXAContent() } bottomSheet(route = Destinations.Sheet_X_B) { BottomSheetXBContent() } ... } ++/pre> ++/code>

Then you just need to navigate with the ++code>navController++/code> to open bottom sheets and navigate between them.

Two hacks we found to enhance Accompanist library

Half expanded state → If we want to skip it?

A ModalBottomSheetLayout can be in 3 states : ++code>hidden, expanded and halfExpanded++/code>. However, it’s very common to skip ++code>halfExpanded++/code> state, and compose-material enables us to do so. Accompanist doesn’t let us do, so we have to create a function for it:

++pre>++code> @ExperimentalMaterialNavigationApi @OptIn(ExperimentalMaterialApi::class) @Composable fun rememberBottomSheetNavigator( sheetState: ModalBottomSheetState ): BottomSheetNavigator { return remember(sheetState) { BottomSheetNavigator(sheetState = sheetState) } }++/pre> ++/code>

Passing a sheetState as a parameter enables us to skip the half expanded state if we want. It enables other possibilities we won’t discuss right now.

I don’t want to navigate back when close a bottomSheet → I just want to close all bottom sheets

By default, when you navigate from a bottom sheet to another, you can use the Android back button to nav back (see Troubleshooting section if it’s not working). But if you close the sheet when you click on the backdrop or when you call ++code>hide()++/code> on the state, you don’t want the previous bottom sheet from the back stack to show up.

Partial solution: navigate back to Home instead of calling ++code>hide()++/code>

++pre>++code> navController.popBackStack( route = Destinations.Home, inclusive = false )++/pre> ++/code>


You can do it easily on button click, but how to do it if the user swipes down the bottom sheet or clicks on backdrop? In these two cases, the ++code>onSheetDismiss++/code> function from accompanist SheetContentHost is called. Unfortunately, we can’t change what it does without forking the library. So let’s fork Google Accompanist !First, We tried to pop to the first back stack entry: but by monkey testing we get the error below. Actually, it happens when I open a bottom sheet while it’s navigating between 2 of them. Indeed, during navigation, we quickly see the Home screen and click on it to open a new sheet. If you are in this case, back stack entries are bugged.

++pre>++code> java.lang.IllegalStateException: Attempted to pop Destination(0x2c8f822d) route=SHEET_X_A, which is not the top of the back stack (Destination(0x2c8f822d) route=SHEET_X_A) at androidx.navigation.NavController.popEntryFromBackStack(NavController.kt:654) ++/pre> ++/code>

As a workaround, I define an optional function that replaces the accompanist pop function when the sheet is dismissed in the BottomSheetNavigator class. I just added the if part, the else is from the original library.

++code>++pre> SheetContentHost( columnHost = columnScope, backStackEntry = latestEntry, sheetState = sheetState, saveableStateHolder = saveableStateHolder, onSheetShown = { backStackEntry -> state.markTransitionComplete(backStackEntry) }, onSheetDismissed = { backStackEntry -> if (customOnSheetDismissed != null) { customOnSheetDismissed() } else { // Sheet dismissal can be started through popBackStack in which case we have a // transition that we'll want to complete if (transitionsInProgressEntries.contains(backStackEntry)) { state.markTransitionComplete(backStackEntry) } else { state.pop(popUpTo = backStackEntry, saveState = false) } } } ) ++/code> ++/pre>

Now I just have to plug it to the navigation controller and pop to Home, same as the partial solution we saw earlier. To do so, I have to initialize the navigation controller before the ++code>BottomSheetNavigator++/code>. Instead of adding the navigator at the initialization of the navigation controller, you add it when the navigator is created. Like this example:

++pre>++code>@ExperimentalMaterialNavigationApi @OptIn(ExperimentalMaterialApi::class) @Composable public fun rememberBottomSheetNavigator( animationSpec: AnimationSpec = SwipeableDefaults.AnimationSpec, sheetState: ModalBottomSheetState = rememberModalBottomSheetState( ModalBottomSheetValue.Hidden, animationSpec ), onSheetDismissed: (() -> Unit)? = null, navController: NavHostController ): BottomSheetNavigator { return BottomSheetNavigator( sheetState = sheetState, customOnSheetDismissed = onSheetDismissed ).apply { navController.navigatorProvider.addNavigator(this) } }++/pre>++/code>

Maybe you can find a solution that doesn’t need to fork accompanist, I think in the ++code>changeDestinationListener++/code> of the ++code>navController++/code>. Let me know if you can think of another solution!

Additional tip when forking a library

To make easier the merge process with our fork when accompanist library is updated, I just made minimal changes to the original files:

  • make the ++code>BottomSheetNavigator++/code> open to be extendable and make my own in my project
  • change private visibility to protected only for attributes I need
  • make ++code>sheetContent++/code> attribute open because it’s the only one I modify
  • change the navigator class for the navigator provider to my custom class 

Troubleshooting

If the back navigation is not working on bottom sheets for your app, maybe another ++code>navController++/code> is receiving this back click, so pass it to the ++code>bottomSheetNavController++/code> by overriding ++code>onBackPress++/code>. It usually happens if your composable is in a fragment managed in a navigation graph. 

Conclusion

Accompanist solution is good but experimental

Google made a great library that simplified our work. But their Accompanist library is still experimental, so we probably will have some breaking changes in the future. Besides, compose bottom sheet itself is annotated as an experimental API. Even if Accompanist is maintained by famous people like Chris Banes or Nick Butcher, it’s still a library that you would move on to in the future, it’s just a temporary solution. If you browse the code, you’ll see some code smells.

If you really don’t want to use or fork the accompanist library, you can still put a ++code>NavHost++/code> in the content of your bottom sheet using material BottomSheetScaffold or ModalBottomSheetLayout. You will just face some problems Accompanist resolves in their library.

Navigating between bottom sheets is a tricky process, I recommend avoiding it if you can. Think about the user experience, if you need to navigate from one sheet to another, maybe you are in a case that needs a full screen flow: is your background screen really necessary ? Is it just to avoid a data reloading when you come back to this screen ? You should read this article to choose the best UX for navigating.

To go further

I’m going to open some pull requests to the library, maybe someday this fork will be not needed.
As for now, I already opened an issue

Resources

Accompanist library:
https://google.github.io/accompanist/navigation-material/

My implementation and sample:
https://github.com/bamlab/android-navigable-bottom-sheet

Développeur Mobile ?

Rejoins nos équipes