Jetpack Compose - Navigate both with and without BottomBar
Asked Answered
T

1

2

i have a Jetpack Compose project that includes navigating using a Scaffold with a BottomBar. By the way, in specific screens, i would like to being able to navigate outside of the innerPadding scope of the Scaffold and just have the composable to be displayed in full screen.

Using official docs approach is fine only until you just need to navigate clicking on the items of the BottomBar.

I found a workaround solution in this post where you use this:

@Composable
fun OneScreen(navController: NavHostController) {
    MainScaffold(
        bottomBar = { BottomBar(navController = navController) },
        content = {
        // content
     })
}

for screens where BottomBar needs to be displayed and this:

@Composable
fun AnotherScreen() {
    MainScaffold ()  {
        //content
    }
}

for the screens where you don't need it to be diplayed.

I call this a "workaround solution" because in this way you need to regenerate the BottomBar on every screen, making it flashing everytime you click on one item because of recomposition (differently from standard BottomBar navigation).

I'm getting the feeling that there should be a more elegant solution, but i lack the experience to be sure of it and i have not been able to find it until now.

EDIT: I was WRONG on a relevant aspect. The flashing is not because of recomposition but because of the Transition Animation (Fade out/Fade in) provided by the Navigation component. Hence, it is possible to eliminate the 'flashing' issue setting No animation on transition. Of course this is still a work-around solution because it limit the user on the flexibility in animating the app.

Turnbull answered 8/3, 2022 at 13:10 Comment(0)
T
1

I found a workaround that i believe to be better than the one linked in the previous post. I decided to use the following approach:

  • Use the NavGraph navigation for all the Screens that don't include BottomBar
  • Use a conditional composable generation for the BottomBar Screen items

In this way i think it is easier to manage the Transition Animations and having readable code. I'm sure this is not the best solution (and i'm not going to mark this as solution) but i think it's worth sharing the approach.

MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyBottomNavTestTheme {
                val navController = rememberNavController()
                SetupNavGraph(navController)
            }
        }
    }
}

NavGraph

const val TEST_ROUTE = "test_route"

@Composable
fun SetupNavGraph(navController: NavHostController) {
    NavHost(
        navController = navController,
        startDestination = Screen.MainScreen.route,
        route = TEST_ROUTE
    ) {
        composable(Screen.MainScreen.route) { MainScreen(navController) }
        composable(Screen.AnotherScreen.route) { AnotherScreen(navController) }
    }
}

MainScreen

@Composable
fun MainScreen(navController: NavHostController){

    var selectedItem by remember { mutableStateOf(0)}

    Scaffold(
        bottomBar = { MyBottomNavBar() {selectedItem = it} }
    ) {
            selectedItem ->
            when (selectedItem){
                0 -> TabOne(navController)
                1 -> TabTwo(navController)
                2 -> TabThree(navController)
            }

    }
}

@Composable
fun MyBottomNavBar(
    onSelectedItem: (Int) -> Unit
) {
    BottomNavigation() {
        BottomNavigationItem(
            selected = true,
            onClick = { onSelectedItem(0) },
            icon = { Icon(imageVector = Icons.Filled.Person, contentDescription = "Person Icon") },
            enabled = true,
        )
        BottomNavigationItem(
            selected = true,
            onClick = { onSelectedItem(1) },
            icon = { Icon(imageVector = Icons.Filled.Phone, contentDescription = "Phone Icon") },
            enabled = true,
        )
        BottomNavigationItem(
            selected = true,
            onClick = { onSelectedItem(2) },
            icon = { Icon(imageVector = Icons.Filled.Place, contentDescription = "Place Icon") },
            enabled = true,
        )
    }
}

@Composable
fun TabOne(navController: NavHostController){
    Surface(
        modifier = Modifier
            .fillMaxSize()
    ){
        Text(
            text = "TabOne"
        )
    }
}

@Composable
fun TabTwo(navController: NavHostController){
    Surface(
        modifier = Modifier
            .fillMaxSize()
    ){
        Text(
            text = "TabTwo"
        )
    }
}

@Composable
fun TabThree(navController: NavHostController){
    Surface(
        modifier = Modifier
            .fillMaxSize()
    ){
        Column(){
            Text(
                text = "TabThree"
            )
            Button(onClick = {navController.navigate(Screen.AnotherScreen.route)}){
                Text("Go to AnotherScreen")
            }
        }

    }
}

AnotherScreen

@Composable
fun AnotherScreen(navController: NavHostController){
    Column(
        verticalArrangement = Arrangement.Center,
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Button(onClick = {navController.navigate(Screen.MainScreen.route)}) {
            Text("Go to MainScreen")
        }
    }
}
Turnbull answered 10/3, 2022 at 13:39 Comment(7)
The problem here is that you are not using navigation to navigate between your tab routesDebbidebbie
Yes, that's exact, but I'm still using Navigation as a "top level navigation". I preferred to preserve the ability to have smooth or customizeable transitions more than managing the BottomBar generation and visibility everytime just to use only Navigation. Btw, as i already said, this is a workaround more than a solution. Still, it works. Any better solution Is more than welcomedTurnbull
Thanks for this. Was fighting between using two NavHosts or hiding the Bottom Nav. So far this solution is working just fine in my applicationCowbell
I have another way inspired from your approach What i am doing is hiding the Bottom Nav as if its item of bottom nav so far no issues in navigation too as logic is in our hand : saurabhjadhavblogs.com/…Politicize
Nice approach and very nice blog. Very readable and well explained. Thanks for sharing :)Turnbull
Hi @SãúrâßhJáðhàv , I am currently using the latest navigation method in my app for navigation where we can pass sealed class instead of route. But in your code you have the code where you take currentDestination like this val navBackStackEntry by navController.currentBackStackEntryAsState() val currentRoute = navBackStackEntry?.destination?.route . Any idea on how to do this using the latest navigation method. Becasue we are passing sealed class instead of string routes, how to get the current destination as a class from navcontroller?Backcourt
@SudheeshMohan there shouldnt be any problem with it, its just you are using sealed classes for simplicity i used strings , meanwhile logic is basically there is list ofbottomroutes :: here you will pass sealed class values and inc urrent route you will mahc it with destination route with this sealedclass bottom nav values , thats it , have you tried it yet ?Politicize

© 2022 - 2024 — McMap. All rights reserved.