Compose Multiplatform Navigation: handle Back button with Navigator BackStack

Compose Multiplatform Navigation: handle Back button with Navigator BackStack
android
Ethan Jackson

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, ) } } }, )

Related Articles