Search code examples
androidandroid-jetpack-composejetpack-compose-navigation

How to handle Android Compose BottomBar Navigation mixed with arguments


I have an Android app using compose navigation. Navigating between its three screens - Home, Calendar, More - is done via a bottom bar:

// onBottomBarItemClick from https://developer.android.com/jetpack/compose/navigation#bottom-nav: 
navController.navigate(destination) {
    popUpTo("HOME") {
        saveState = true
    }
    launchSingleTop = true
    restoreState = true
}

Up until now everything is working as expected.

However, i sometimes want to pass an argument from Home to Calendar - see screenshot. This is where things start to break apart.

HomeScreen(
    onNavigateToCalendar = { argument ->
        navController.navigate("CALENDAR?ARG=$argument") {
            popUpTo("HOME") {
                saveState = true
            }
            launchSingleTop = true
            restoreState = false
        }
    }
)

If i now do the following...

  • Start the app fresh - i'm at home
  • Navigate to Calendar with argument A - it shows A
  • Navigate to Home via BottomBar
  • Navigate to Calendar with argument B - it shows B
  • Navigate to More
  • Navigate to Calendar via BottomBar - it shows A not B - which is weird.

I believe i have tried every possible combination of saveState / launchSingleTop / restoreState, but all of them had some issues. Can someone help me please? I'm loking for a solution where:

  • Calendar will always display an argument when explicitly provided
  • Calendar will display none or the last argument, when no argument is provided
  • "Back" works correctly: Navigating back from Calendar / More should lead to Home
  • State (e.g. scroll state) is kept as you'd expect

enter image description here


Minimal Example:

@Composable
fun MainScreen() {
    val navController = rememberNavController()

    Scaffold(
        content = { paddingValues ->
            NavHost(
                navController = navController,
                startDestination = "HOME",
                modifier = Modifier.padding(paddingValues)
            ) {
                composable("HOME") {
                    HomeScreen(
                        onNavigateToCalendar = { argument ->
                            navController.navigate("CALENDAR?ARG=$argument") {
                                popUpTo("HOME") {
                                    saveState = true
                                }
                                launchSingleTop = true
                                restoreState = false
                            }
                        }
                    )
                }

                composable("CALENDAR?ARG={ARG}") {
                    CalendarScreen(it.arguments?.getString("ARG"))
                }

                composable("MORE") {
                    MoreScreen()
                }
            }
        },
        bottomBar = {
            MyBottomBar(
                onClick = { destination ->
                    navController.navigate(destination) {
                        popUpTo("HOME") {
                            saveState = true
                        }
                        launchSingleTop = true
                        restoreState = true
                    }
                }
            )
        }
    )
}

@Composable
private fun HomeScreen(
    onNavigateToCalendar: (argument: String) -> Unit
) {
    Column {
        Text("HOME")

        Button(
            onClick = { onNavigateToCalendar("A") },
            content = { Text("Navigate to CALENDAR with argument = A") },
        )

        Button(
            onClick = { onNavigateToCalendar("B") },
            content = { Text("Navigate to CALENDAR with argument = B") },
        )
    }
}

@Composable
private fun CalendarScreen(argument: String?) {
    Text("CALENDAR with argument = $argument")
}

@Composable
private fun MoreScreen() {
    Text("MORE")
}

@Composable
private fun MyBottomBar(
    onClick: (destination: String) -> Unit
) {
    BottomAppBar {
        listOf("HOME", "CALENDAR", "MORE").forEach { destination ->
            BottomNavigationItem(
                selected = false, // TODO - not important for now
                icon = {},
                label = { Text(destination) },
                onClick = { onClick(destination) },
            )
        }
    }
}

Solution

  • When navigating to your Calendar screen sending a specific parameter (either A or B, via the onClick listener) you need to clear the backStack of your route so the new state can be saved correctly. Like this:

    onNavigateToCalendar = { argument ->
       navController.clearBackStack("CALENDAR?ARG={ARG}")
       navController.navigate("CALENDAR?ARG=$argument") {
            ...
       }
    }
    

    For more details you can check their official comment here: https://issuetracker.google.com/issues/294408574

    To me it is implicit that I want to override the previous value by using savingState=true and restoreState=false, but seems they don't save any state if you didn't restored the previous one... so you need to clear the backstack manually before going to your screen