xxfast / Decompose-Router

A Compose-multiplatform navigation library that leverage Decompose to create an API inspired by Conductor
https://xxfast.github.io/Decompose-Router/
221 stars 9 forks source link

How to open a ModalBottomSheet with the previous route behind #72

Closed GabriellCosta closed 3 months ago

GabriellCosta commented 10 months ago

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

xxfast commented 10 months ago

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

GabriellCosta commented 10 months ago

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 commented 10 months ago

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")
                        }
                    }
                }

            }
        }
xxfast commented 10 months ago

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()
    }
  }
}
GabriellCosta commented 10 months ago

Hello @xxfast, sorry for the delay

Yes, something like that would be great

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

arkivanov commented 10 months ago

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.

xxfast commented 7 months ago

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 commented 7 months ago

@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

GabriellCosta commented 7 months ago

Hello @xxfast

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

xxfast commented 3 months ago

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