Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
GabriellCosta opened this issue Oct 15, 2023 · 10 comments
Closed

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

GabriellCosta opened this issue Oct 15, 2023 · 10 comments
Labels
question Further information is requested

Comments

@GabriellCosta
Copy link
Contributor

GabriellCosta commented Oct 15, 2023

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 xxfast added the question Further information is requested label Oct 19, 2023
@xxfast
Copy link
Owner

xxfast commented Oct 19, 2023

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
Copy link
Contributor Author

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
Copy link
Contributor Author

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
Copy link
Owner

xxfast commented Oct 22, 2023

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

pscore.mp4

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
Copy link
Contributor Author

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
Copy link
Contributor

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
Copy link
Owner

xxfast commented Jan 23, 2024

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
Copy link
Owner

xxfast commented Jan 23, 2024

@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
Copy link
Contributor Author

Hello @xxfast

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

@xxfast
Copy link
Owner

xxfast commented May 23, 2024

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

@xxfast xxfast closed this as completed May 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
question Further information is requested
Projects
None yet
Development

No branches or pull requests

3 participants