Compose Multiplatform Navigation: handle Back button with Navigator BackStack
I am working on an application, which has a complex nav graph.
The problem is How to handle back button in topAppBar in this situation?
I want to use navController to know it's navigated from MainNav or from other features? to know show back button in topAppBar or not.
//RootNav(no-screen) -> {SplashNav(), MainNav()}
//SplashNav(multiple-screens) -> {MainNav()}
MainNav(no-screen, just BottomBar and NavHost) -> {Feature1Nav(), Feature2Nav(), Feature3Nav()}
Feature1Nav(multiple-screens) -> {Feature2Nav(), Feature3Nav()}
Feature2Nav(multiple-screens) -> {Feature1Nav(), Feature3Nav()}
//Feature3Nav(multiple-screens) -> {Feature1Nav(), Feature2Nav()}
For example if I navigated from
Feature1ListScreen -> Feature2ListScreen -> Feature2DetailScreen
then I should show back button in 2nd and 3rd screen but if I navigated from
Feature2ListScreen -> Feature2DetailScreen
back button only visible in details screen
also there is some of my codes:
feature1:
@Composable
fun Feature1Nav(
paramFromFeature2: Long? = null,
detailId: Long?,
navigateToFeature2: (paramFromFeature1: Long) -> Unit,
) {
val navigator = rememberNavController()
NavHost(startDestination =
if (detailId != null)
Feature1NavRoots.Feature1Detail.route.plus("/${detailId}")
else
Feature1NavRoots.Feature1Listl.route),
navController = navigator,
modifier = Modifier.fillMaxSize()
) {
composable(route = Feature1NavRoots.Projects.route) { backStackEntry ->
Feature1ListScreen(
popUp = {
navigator.popBackStack()
},
navigateToDetail = { id: Long ->
navigator.currentBackStackEntry?.arguments?.putLong("id", id)
navigator.navigate(
Feature1NavRoots.Feature1Detail.route.plus("/$id")
)
},
navigateToFeature2 = {
navigator.currentBackStackEntry?.arguments?.putLong("paramFromFeature1", it)
navigator.navigate(
Feature1NavRoots.Feature2Nav.route.plus("?paramFromFeature1={it}")
)
},
)
}
composable(route = Feature1NavRoots.ProjectDetail.route.plus("/{id}")) { backStackEntry ->
val argument = backStackEntry.arguments
val argId: Long? = argument?.getArgumentLong("id")
argId?.let {
Feature1DetailScreen(
projectId = argId,
popUp = {
navigator.popBackStack()
},
)
}
}
composable(route = Feature1NavRoots.Feature2Nav.route.plus("?paramFromFeature1 ={paramFromFeature1}")) { backStackEntry ->
val argument = backStackEntry.arguments
val argId: Long? = argument?.getArgumentLong("paramFromFeature1")
Feature2Nav(
paramFromFeature1 = argId,
navigateToFeature1 = {
navigator.popBackStack()
},
navigator = navigator,
popUp = {
navigator.popBackStack()
}
)
}
}
}
feature2:
@Composable
fun Feature2Nav(
paramFromFeature1: Long? = null,
navigator: NavHostController = rememberNavController(),
popUp: () -> Unit,
detailId: Long?,
navigateToFeature1: (id: Long) -> Unit,
) {
NavHost(
startDestination =
if (detailId != null)
Feature2NavigationRoots.Feature2Detail.route.plus("/${detailId}")
else
Feature2NavigationRoots.Feature2List.route,
navController = navigator,
modifier = Modifier.fillMaxSize()
) {
composable(route = Feature2Roots.Feature2List.route) { backStackEntry ->
Feature2ListScreen(
filter = filter,
popUp = {
if (navigator.previousBackStackEntry == null) {
popUp() // Pop from MainNav if at the root
} else {
navigator.popBackStack() // Pop within Feature1Nav
}
},
navigateToProjects = navigateToProjects,
navigateToDetail = { id: Long ->
navigator.navigate(Feature2Roots.Feature2Detail.route.plus("/$id"))
}
)
}
composable(route = Feature2Roots.Feature2Detail.route.plus("/{id}")) { backStackEntry ->
val argument = backStackEntry.arguments
val id = argument?.getArgumentLong("id")
id?.let {
Feature2DetailScreen(
customerEnquiryId = id,
popUp = {
navigator.popBackStack()
},
navigateToProjects = navigateToProjects,
)
}
}
}
}
MainNav:
@Composable
fun MainScreen(logout: () -> Unit) {
Scaffold { innerPadding ->
val navigator = rememberNavController()
val navBackStackEntry by navigator.currentBackStackEntryAsState()
// val currentRoute = navBackStackEntry?.destination?.route
// val containerColor = MaterialTheme.colorScheme.background
// val selectedColor = MaterialTheme.colorScheme.primary
// val unselectedColor = MaterialTheme.colorScheme.onBackground
PermanentNavigationDrawer(
modifier = Modifier.fillMaxSize(),
drawerContent = {
PermanentDrawerSheet(
modifier = Modifier.width(200.dp).fillMaxHeight()
.verticalScroll(state = rememberScrollState()),
drawerContainerColor = containerColor,
) {
val items = listOf(
MenuItems.Feature1,
MenuItems.Feature2,
)
items.forEach {
NavigationDrawerItem(label = { Text(text = it.title) },
colors = NavigationDrawerItemDefaults.colors(
selectedContainerColor = containerColor,
unselectedContainerColor = containerColor,
selectedIconColor = selectedColor,
unselectedIconColor = unselectedColor,
selectedTextColor = selectedColor,
unselectedTextColor = unselectedColor,
selectedBadgeColor = selectedColor,
unselectedBadgeColor = unselectedColor,
),
selected = it.route == currentRoute,
icon = {
Icon(
painterResource(if (it.route == currentRoute) it.selectedIcon else it.unSelectedIcon),
it.title
)
},
onClick = {
if (currentRoute != it.route) {
navigator.navigate(it.route) {
navigator.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
launchSingleTop = true
restoreState = true
}
} else {
navigator.navigate(it.route) {
popUpTo(it.route) {
saveState = true
}
}
}
})
}
}
},
content = {
Box(modifier = Modifier.padding(innerPadding)) {
NavHost(
startDestination = MainNavigationRoots.Home.route,
navController = navigator,
modifier = Modifier.fillMaxSize()
) {
composable(route = MainNavigationRoots.Feature1.route) {
Feature1Nav(
navigateToFeature2 = { paramFromFeature1: Long ->
navigator.currentBackStackEntry?.arguments?.putLong(
"paramFromFeature1",
paramFromFeature1
)
navigator.navigate(
route = MainNavigationRoots.Feature2.route,
) {
navigator.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
inclusive = true
}
}
// launchSingleTop = true
restoreState = true
}
},
)
}
composable(route = MainNavigationRoots.Feature2.route) {
Feature2Nav(
filter = FilterParams(),
navigateToFeature1 = { paramFromFeature2: Long ->
navigator.currentBackStackEntry?.arguments?.putLong(
"paramFromFeature2",
paramFromFeature2
)
navigator.navigate(
route = MainNavigationRoots.Feature1.route,
)
},
popUp = {
navigator.popBackStack()
}
)
}
}
}
}
)
}
}
I changed names, if you see: Project is feature1 CustomerEnquiry is feature2
Answer
I am doing it inside topBar.
AdaptiveScaffold(
modifier = Modifier.fillMaxSize(),
topBar = {
if (topBarVisibility.value)
ThesisTopBar(
backArrowVisibility = topBarBackArrowVisibility.value,
navController = navController,
)
},
bottomBar = {
if (bottomBarVisible.value)
BottomNavigationBar(navController = navController)
},
) {
NavHost(
navController = navController,
startDestination = INTRO_SPLASH,
modifier = Modifier.padding(it),
enterTransition = { EnterTransition.None},
exitTransition = { ExitTransition.None }
){
introductionGraph(navController)
mainGraph(navController)
}
}
Just passing navController into topBar and adding IconButton into navigationIcon
AdaptiveTopAppBar(
title = {
Text(
text = "Some Text",
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
},
navigationIcon = {
if (backArrowVisibility) {
IconButton(onClick = { navController.popBackStack() }) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
},
)