Decompose-Router icon indicating copy to clipboard operation
Decompose-Router copied to clipboard

How to open a ModalBottomSheet with the previous route behind

Open GabriellCosta opened this issue 2 years ago • 9 comments

I would like to start a ModalBottomSheet (android) as part of my navigation routes (some action would trigger and start a specific ModalBottomSheet) and that one would be treated as a route to be used, similar to what I would do in Compose Navigation

What I did was start the route normally and that works, the problem is that the screen behind the BottomSheet is blank, as my previous screen is in the stack

How Can I present a ModalBottomSheet as an overlay over other screens?

obs: I was not able to put the Question label in this issue

GabriellCosta avatar Oct 15 '23 17:10 GabriellCosta

Hi @GabriellCosta

Is there a reason why this needs to be a route? Can you show how you are currently handling this (with androidx-navigation-compose)

IMO, modals (like dialogs or modal bottom sheets) typically do not need to be routes as they are not part of the navigation graph. But, I can be convinced otherwise if there's a valid use case for it to be considered as a route

xxfast avatar Oct 19 '23 21:10 xxfast

Hello @xxfast

We have here some BottomSheet and dialogs that are part of the flow and treat them today as part of the navigation as they could be open from different places, we could just open it as a normal dialog, but the idea was to treat some of these these BottomSheet as self-contained and give them the same attention we give to screens

With NavigationCompose this is doable today as we can create a BottomSheet as part of the routes

As we can have some BottomSheet dialogs as complex as we need or as simple as we need, and let the navigation be there, if in the future this needs to be a screen instead of a dialog, we can just go there and change the implementation, as the navigation is already being made as route navigation, this way the navigation route is abstracted from the implementation detail of what each route is, as one route should not care about the details of another

GabriellCosta avatar Oct 21 '23 08:10 GabriellCosta

Here a simple example

setContent {
            MyApplicationTheme {
                // A surface container using the 'background' color from the theme
                val bottomSheetNavigator = rememberBottomSheetNavigator()
                val navHost = rememberNavController(bottomSheetNavigator)

                val sheetState = bottomSheetNavigator.navigatorSheetState
                val current = LocalContext.current

                ModalBottomSheetLayout(
                    bottomSheetNavigator = bottomSheetNavigator
                ) {
                    NavHost(
                        navController = navHost,
                        startDestination = "home",
                    ) {
                        composable("home") {
                            Home { dest ->
                                navHost.navigate(dest)
                            }
                        }

                        composable("greetins/{id}") {
                            GreetingComposable(text = it.arguments?.getString("id").orEmpty())
                        }

                        bottomSheet(route = "sheet") {
                            OnDismissAction(sheetState) {
                                Toast.makeText(current, "sheet", Toast.LENGTH_LONG).show()
                            }

                            Text("This is a cool bottom sheet!")
                        }

                        bottomSheet(route = "sheet2") {
                            OnDismissAction(sheetState) {
                                Toast.makeText(current, "sheet2", Toast.LENGTH_LONG).show()
                            }

                            Text("This is the other sheet")
                        }
                    }
                }

            }
        }

GabriellCosta avatar Oct 21 '23 08:10 GabriellCosta

Hi, @GabriellCosta Thank you for the detailed explanation of your use case.

Is this from accompanist's navigation-material?

The equivalent API from decompose to support this use case would be child slots, which requires some breaking API changes to accommodate on the router API side. I think this is a good use case that warrants such a breaking API change - so I'm definitely up for it. Just a few more questions for me to fully wrap my head around the requirement

In my production app, we have a modal bottom sheet that looks like this

https://github.com/xxfast/Decompose-Router/assets/13775137/2b248a7c-b2c0-409b-8db5-c837299797ff

The way this is currently implemented looks something like this

@Composable
fun TasksRootScreen() {
  val router: Router<TasksRootScreens> = rememberRouter(TasksRootScreens::class) { listOf(TasksRootScreens.Home) }
  
  RoutedContent(router = router) { screen ->
    when (screen) {
      is Home -> TasksHomeScreen(..)
      is IncidentDetails -> IncidentDetailsScreen()
    }
  }
}

the filter bottom sheet is implemented within TasksHomeScreen,

@Composable
fun TasksHomeScreen() {
  val sheetState: ModalBottomSheetState =
    rememberModalBottomSheetState(Hidden, skipHalfExpanded = true)
    
  ModalBottomSheetLayout(
    sheetState = sheetState,
    sheetContent = {
      TasksFilterScreen(
        onClosed = { coroutineScope.launch { sheetState.hide() } },
      )
    },
  ) { .. }
}

If I understand this correctly, in your case you want this bottom sheet to exist outside of the main screen? Something like

fun TasksRootScreen() {
  val router: Router<TasksRootScreens> = rememberRouter(TasksRootScreens::class) { listOf(TasksRootScreens.Home) }
  
  RoutedContent(router = router) { screen ->
    when (screen) {
      is Home -> TasksHomeScreen(..)
      is FIlter -> TasksHomeFilterScreen(..)
      is IncidentDetails -> IncidentDetailsScreen()
    }
  }
}

xxfast avatar Oct 22 '23 04:10 xxfast

Hello @xxfast, sorry for the delay

Yes, something like that would be great

As I understand our RoutedContent would need to support ChildSlots right?

GabriellCosta avatar Oct 31 '23 22:10 GabriellCosta

I would place those bottoms sheets as a nested navigation, e.g. inside the Home screen. Placing everything at the top level doesn't look scalable, e.g. there could be 100s of bottom sheets in an app.

Perhaps, Decompose-Router could add a separate API for this kind of navigation, i.e. Slot or Overlay where the hosting Composable is still visible. I think this could be done even without breaking the existing API.

arkivanov avatar Nov 06 '23 21:11 arkivanov

Hi @GabriellCosta. With latest 0.7.0-SNAPSHOT you now can use a router for pages & slots. (as implemented in #85)

Here's how you would use router for slots

@Serialisable object ShowBottomSheet

@Composable
fun SlotScreen() {
  val router: Router<ShowBottomSheet> = rememberRouter(ShowBottomSheet::class, initialConfiguration =  { null })
  // An example button to open the bottom sheet
  Button(
    onClick = { router.activate(ShowBottomSheet) },
  ) {
    Text("Show Bottom Sheet")
  }
  
  RoutedContent(router) { screen ->
    ModalBottomSheet(
      onDismissRequest = { router.dismiss() },
    ) {
      // sheet content
    }
  }
}

As @arkivanov pointed out, you will need to handle these as nested navigation modals and you won't be able to handle everything at the top level.

Let me know if this can address your usecase

xxfast avatar Jan 23 '24 03:01 xxfast

@GabriellCosta If you want to handle everything at the root level - here's how I would handle your case mentioned here, and I don't think you'd need a slot for that

@Serialisable sealed class Screen { 
 data object Home 
 data class Greetings(val id: String)
 data object Sheet 
}

val router: Router<Screen> = rememberRouter(initialStack = { listOf(Home) }

RoutedContent(router = router) { screen ->
  when(screen){
    Home -> HomeScreen(onGreeting = { id -> router.push(Greetings(id)) })
    is Greetings -> GreetingScreen(screen.id)
    Sheet -> SheetScreen(onDismiss = { router.pop() })
  }
}

@Composable
fun SheetScreen(onDismiss: () -> Unit) {
   ModalBottomSheet(
      onDismissRequest = onDismiss,
    ) {
      // sheet content
    }
}

I believe the sheet should be rendered over the previous screen. Let me know if this works

xxfast avatar Jan 23 '24 03:01 xxfast

Hello @xxfast

thanks for letting me know, I will try it here and add a feedback, thanks : D

GabriellCosta avatar Jan 24 '24 16:01 GabriellCosta

Feel free to comment on this issue with any feedback. Going to close this issue for now

xxfast avatar May 23 '24 09:05 xxfast