Search code examples
android-jetpack-composeandroid-jetpack-navigationandroid-bottomnav

Jetpack Compose - Navigate both with and without BottomBar


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.


Solution

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