How navigate back with new argument in Jetpack Compose? - android

I want to navigate back with different input argument.
I have a ParentScreen and it is using input argument arg (="first"). Code navigates from parent to child by sending newArg (="firstsecond") on button press. When navigating back from child to parent I want parent's input arg to be "fistsecond".
Below I wrote the code (it is not working) to show what I want.
#Composable
fun ParentScreen(
nav: NavHostController,
arg: String = "first"
) {
val newArg = arg + "second"
Button(onClick = {
nav.navigate("child/$newArg") {
popUpTo("parent/$newArg") {
saveState = true
}
}
}) {
Text(text = arg)
}
}

What you may want to do is launch the parent composable as single top, and when navigating from child composable, you can send the new argument and launch it as single top again. Here is how you can achieve if you are using compose navigation;
You can launch ParentScreen for the first time like this;
navController.navigate("parent/first") {
launchSingleTop = true
}
Then navigate to ChildScreen as usual;
navController.navigate("child/firstSecond")
In ChildScreen navigate to ParentSceen like this, this will remove the ChildScreen from the nav stack and launch the ParentScreen as single top, resulting only ParentScreen in the stack with new parameter;
navController.navigate("parent/$newArg") {
launchSingleTop = true
popUpTo("child/{someArg}") {
inclusive = true
}
}
Full code here;
#Composable
fun SampleNavHost(
modifier: Modifier = Modifier,
navController: NavHostController,
onBackClick: () -> Unit,
startDestination: String = "start_dest",
) {
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable("start_dest") {
Button(onClick = {
navController.navigate("parent/first") {
launchSingleTop = true
}
}) {
Text(text = "move to parent")
}
}
composable("parent/{someArg}") {
val arg = it.arguments?.getString("someArg").orEmpty()
ParentScreen(
title = arg,
navStackSize = navController.backQueue.size.toString(),
onButtonClick = {
navController.navigate("child/firstSecond")
}
)
}
composable("child/{someArg}") {
val arg = it.arguments?.getString("someArg").orEmpty()
val newArg = arg + "second"
ChildScreen(
title = arg,
onButtonClick = {
navController.navigate("parent/$newArg") {
launchSingleTop = true
// this will navigate current composable from stack when navigating
popUpTo("child/{someArg}") {
inclusive = true
}
}
}
)
}
}
}
#Composable
fun ParentScreen(
title: String,
navStackSize: String,
onButtonClick: () -> Unit,
) {
Column {
Text(text = "Parent Screen, stack size: $navStackSize")
Button(onClick = onButtonClick) {
Text(text = title)
}
}
}
#Composable
fun ChildScreen(
title: String,
onButtonClick: (String) -> Unit,
) {
Column {
Text(text = "Child Screen")
Button(onClick = { onButtonClick(title) }) {
Text(text = title)
}
}
}

Related

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

Kotlin Jetpack Compose BottomNavigation selected page is not been updated

I have BottomNavigation wrapped around as a BottomBar composable and inside my MainScreen composable.
The current implementation renders properly and I am able to navigate between different Pages. However, the issue I am facing is that the selected property in the BottomNavigationItem is not been updated correctly when changing the current page. This causes the UI to be located in the "Profile" page, but the bottom bar highlights the "Home" page.
I am almost sure it is something regarding the state of the composable, and I tried passing the NavHostController all the way down to the BottomBar, but that didn't solved the issue.
enum class Page(
#StringRes val title: Int,
val isCorePage: Boolean,
val icon: ImageVector) {
Profile(
title = R.string.profile_page_title,
isCorePage = true,
icon = Icons.Filled.Person),
Home(
title = R.string.home_page_title,
isCorePage = true,
icon = Icons.Filled.Home),
... etc
}
#Composable
fun BottomBar(
currentPage: Page,
onClick: (page: Page) -> Unit = { }) {
BottomNavigation {
Page.values().forEach { page ->
if(page.isCorePage) {
val title = stringResource(page.title);
BottomNavigationItem(
icon = { Icon(
imageVector = page.icon,
contentDescription = title) },
label = { Text(text = title) },
selected = page == currentPage,
onClick = { onClick(page) }
)
}
}
}
}
#Composable
fun MainScreen(
sessionState: State<Session>,
navController: NavHostController = rememberNavController()) {
val backStackEntry by navController.currentBackStackEntryAsState()
val scaffoldState = rememberScaffoldState()
val scope = rememberCoroutineScope()
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopBar(
title = backStackEntry?.destination?.route ?: "Title",
menuClick = { scope.launch { scaffoldState.drawerState.open() } },
searchClick = { navController.navigate(Page.Search) },
notificationsClick = { navController.navigate(Page.Notifications) })
},
drawerContent = { SideDrawer(sessionState = sessionState) },
bottomBar = {
BottomBar(
currentPage = navController.currentPageOrDefault(Page.Home),
onClick = { page -> navController.navigate(page) })
}
) {
PageNavHost(navController)
}
}
fun NavHostController.navigate(page: Page) {
val navController = this
navController.navigate(page.name) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// re-selecting the same item
launchSingleTop = true
// Restore state when re-selecting a previously selected item
restoreState = true
}
}
fun NavHostController.currentPageOrDefault(default: Page): Page {
return this.currentPage() ?: default
}
fun NavHostController.currentPage(): Page? {
val currentRoute = this.currentDestination?.route ?: return null;
Page.values().forEach { page ->
if(page.name == currentRoute) {
return page
}
}
return null
}

Jetpack Compose: navigate to X screen (jump) keeping back-stack history

Have Activity which holds NavHostController, when activity starts, need to navigate X screen, but pressing back need to navigate to previous screen, not to the start destination (because navigating to X was from startDest).
#OptIn(ExperimentalMaterialNavigationApi::class)
#Composable
private fun NavHostController(
navController: NavHostController,
startDest: String = "screen 1"
) {
NavHost(
navController = navController,
startDestination = startDest,
) {
composable("screen 1") {}
composable("screen 2") {}
...
composable("screen N") {}
}}
EXAMPLE:
have screens like: "screen 1-2-3-4-5..N"
and the "screen 1" is start destination,
activity starts and nav-ctrl jumps to screen 5(for example),
and the back press need to work like: to 4, 3, 2.. NOT direct 1 (as start destination is 1)
Question:
How to set back stack history in nav controller
OR
How navigate (jump) with keeping back stack order.
To achieve this you will need to use the BackHandler and make some check to see if the previous back stack route is the screen that should be the destination when using the back button. For example:
#Composable
fun ScreenOne(
nextScreen: () -> Unit
) {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "ScreenOne")
Button(onClick = nextScreen) {
Text(text = "Next")
}
}
}
#Composable
fun ScreenTwo(
back: () -> Unit,
nextScreen: () -> Unit
) {
// to intercept the click on the back button of the android navigation bar
BackHandler(onBack = back)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "ScreenTwo")
Button(onClick = back) {
Text(text = "Back")
}
Button(onClick = nextScreen) {
Text(text = "Next")
}
}
}
#Composable
fun ScreenThree(
back: () -> Unit
) {
// to intercept the click on the back button of the android navigation bar
BackHandler(onBack = back)
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "ScreenThree")
Button(onClick = back) {
Text(text = "Back")
}
}
}
ScreenOne just have the option to navigate to ScreenTwo.
ScreenTwo have the option to navigate (back) to ScreenOne and navigate to ScreenThree.
ScreenThree just have the option to navigate (back) to ScreenTwo.
Extension to navigate back:
fun NavHostController.navigateBack(
targetRoute: String,
currentRoute: String
) {
val previousRoute = previousBackStackEntry?.destination?.route ?: "null"
// if the previous route is what we want, just go back
if (previousRoute == targetRoute) popBackStack()
// otherwise, we do the navigation explicitly
else navigate(route = targetRoute) {
// remove the entire backstack up to this this route, including herself
popUpTo(route = currentRoute) { inclusive = true }
launchSingleTop = true
}
}
NavHost:
NavHost(
modifier = Modifier.fillMaxSize(),
navController = navController,
startDestination = "screen_three"
) {
composable(route = "screen_one") {
ScreenOne(
nextScreen = {
navController.navigate(route = "screen_two") {
launchSingleTop = true
}
}
)
}
composable(route = "screen_two") {
ScreenTwo(
back = {
navController.navigateBack(
targetRoute = "screen_one",
currentRoute = "screen_two"
)
},
nextScreen = {
navController.navigate(route = "screen_three") {
launchSingleTop = true
}
}
)
}
composable(route = "screen_three") {
ScreenThree(
back = {
navController.navigateBack(
targetRoute = "screen_two",
currentRoute = "screen_three"
)
}
)
}
}

Jetpack compose navigation closes app instead of returning to previous screen

I have implemented the accompanist navigation animation library in my project and have stumbled into two issues. The first issue is that the animations aren't being applied when navigating from one screen to another. The second issue is that the "back" of the system closes the app to the background instead of returning to the previous screen.
Here is the layout of the app starting from the MainActivity.
MainActivity.kt
#ExperimentalAnimationApi
#AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private val preDrawListener = ViewTreeObserver.OnPreDrawListener { false }
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.MyHomeTheme)
super.onCreate(savedInstanceState)
WindowCompat.setDecorFitsSystemWindows(window, false)
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.addOnPreDrawListener(preDrawListener)
lifecycleScope.launch {
setContent {
val systemUiController = rememberSystemUiController()
SideEffect {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = true
)
systemUiController.setNavigationBarColor(
color = Color(0x40000000),
darkIcons = false
)
}
MyHomeApp(
currentRoute = Destinations.Welcome.WELCOME_ROUTE
)
}
unblockDrawing()
}
}
private fun unblockDrawing() {
val content: View = findViewById(android.R.id.content)
content.viewTreeObserver.removeOnPreDrawListener(preDrawListener)
content.viewTreeObserver.addOnPreDrawListener { true }
}
}
MyHomeApp.kt
#ExperimentalAnimationApi
#Composable
fun MyHomeApp(currentRoute: String) {
MyHomeTheme {
ProvideWindowInsets {
val navController = rememberAnimatedNavController()
val scaffoldState = rememberScaffoldState()
val darkTheme = isSystemInDarkTheme()
val items = listOf(
HomeTab.Dashboard,
HomeTab.Details,
HomeTab.Settings
)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val bottomPaddingModifier = if (currentDestination?.route?.contains("welcome") == true) {
Modifier
} else {
Modifier.navigationBarsPadding()
}
Scaffold(
modifier = Modifier
.fillMaxSize()
.then(bottomPaddingModifier),
scaffoldState = scaffoldState,
bottomBar = {
if (currentDestination?.route in items.map { it.route }) {
BottomNavigation {
items.forEach { screen ->
BottomNavigationItem(
label = { Text(screen.title) },
icon = {},
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
)
}
}
}
}
) { innerPadding ->
MyHomeNavGraph(
modifier = Modifier.padding(innerPadding),
navController = navController,
startDestination = navBackStackEntry?.destination?.route ?: currentRoute
)
}
}
}
}
sealed class HomeTab(
val route: String,
val title: String
) {
object Dashboard : HomeTab(
route = Destinations.Home.HOME_DASHBOARD,
title = "Dashboard"
)
object Details : HomeTab(
route = Destinations.Home.HOME_DETAILS,
title = "Details"
)
object Settings : HomeTab(
route = Destinations.Home.HOME_SETTINGS,
title = "Settings"
)
}
MyHomeNavGraph.kt
#ExperimentalAnimationApi
#Composable
fun MyHomeNavGraph(
modifier: Modifier = Modifier,
navController: NavHostController,
startDestination: String
) {
val actions = remember(navController) { Actions(navController = navController) }
AnimatedNavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
composable(
route = Destinations.Welcome.WELCOME_ROUTE,
enterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_LOGIN_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) {
WelcomeScreen(
navigateToLogin = actions.navigateToWelcomeLogin,
navigateToRegister = actions.navigateToWelcomeRegister,
)
}
composable(
route = Destinations.Welcome.WELCOME_LOGIN_ROUTE,
enterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
exitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Left, animationSpec = tween(700))
else -> null
}
},
popEnterTransition = {
when (initialState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideIntoContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
},
popExitTransition = {
when (targetState.destination.route) {
Destinations.Welcome.WELCOME_ROUTE ->
slideOutOfContainer(towards = AnimatedContentScope.SlideDirection.Right, animationSpec = tween(700))
else -> null
}
}
) {
WelcomeLoginScreen(
// Arguments will be passed to navigate to the home screen or other
)
}
}
}
class Actions(val navController: NavHostController) {
// Welcome
val navigateToWelcome = {
navController.navigate(Destinations.Welcome.WELCOME_ROUTE)
}
val navigateToWelcomeLogin = {
navController.navigate(Destinations.Welcome.WELCOME_LOGIN_ROUTE)
}
}
For simplicity's sake, you can assume that the screens are juste a box with a button in the middle which executes the navigation when they are clicked.
The accompanist version I am using is 0.24.1-alpha (the latest as of this question) and I am using compose version 1.2.0-alpha02 and kotlin 1.6.10.
In terms of animation, the only difference I can see with the accompanist samples is that I don't pass the navController to the screens but I don't see how that could be an issue.
And in terms of using the system back which should return to a previous, I'm genuinely stuck in terms of what could cause the navigation to close the app instead of going back. On other projects, the system back works just fine but not with this one. Is the use of the accompanist navigation incompatible ? I'm not sure.
Any help is appreciated!
I found the source of the issue.
The fact that I was setting the startDestination parameter to navBackStackEntry?.destination?.route ?: currentRoute meant that each change to the navBackStackEntry recomposed the MyHomeNavGraph and hence the backstack was reset upon the recomposition.
Note to self, watch out when copying navigation from multiple sources!

Scaffold with TopAppBar integration with Navigation

How to show navigation icon (BackArrow or Menu) in TopAppBar using Scaffold based on actual position in NavController? I am using Navigating with Compose 1.0.0-alpha02. Below is a sample code with a description of how it should work
#Composable
fun App()
{
val navController = rememberNavController()
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = "App title") },
navigationIcon = {
/*
Check if navController back stack has more
than one element. If so show BackButton.
Clicking on that button will move back
*/
val canMoveBack = true
if (canMoveBack)
{
IconButton(onClick = {
// Move back
navController.popBackStack()
}) {
Icon(asset = Icons.Outlined.ArrowBack)
}
}
else
{
IconButton(onClick = {
// show NavDrawer
}) {
Icon(asset = Icons.Outlined.Menu)
}
}
},
)
},
bodyContent = {
AppBody(navController)
}
)
}
I thought about something like navController.backStack.size but I got error NavController.getBackStack can only be called from within the same library group (groupId=androidx.navigation).
And the second question, if I wanted to change the TopAppBar text do I have to hoist this text and give every "screen" possibility to change this text, or is there any easy built-in way to do this like in the standard View System?
Thanks to Abdelilah El Aissaoui I have got an idea of how to do it with one Scaffold and just changing bodyContent. In this method, we don't have to pass navController to any body element, everything is done in base App composable. Below is code which enables to navigate between two bodies (Lesson -> Student)
App:
#Composable
fun App(
viewModel: MainViewModel
)
{
val navController = rememberNavController()
val baseTitle = "" // stringResource(id = R.string.app_name)
val (title, setTitle) = remember { mutableStateOf(baseTitle) }
val (canPop, setCanPop) = remember { mutableStateOf(false) }
val scaffoldState: ScaffoldState = rememberScaffoldState()
navController.addOnDestinationChangedListener { controller, _, _ ->
setCanPop(controller.previousBackStackEntry != null)
}
// check navigation state and navigate
if (viewModel.navigateToStudents.value)
{
navController.navigate(route = STUDENT_SCREEN_ROUTE)
viewModel.studentsNavigated()
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = title) },
navigationIcon = {
if (canPop)
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(asset = Icons.Outlined.ArrowBack)
}
}
else
{
IconButton(onClick = {
scaffoldState.drawerState.open()
}) {
Icon(asset = Icons.Outlined.Menu)
}
}
},
)
},
scaffoldState = scaffoldState,
drawerContent = {
DrawerContent()
},
bodyContent = {
AppBody(
viewModel = viewModel,
navController = navController,
setTitle = setTitle
)
}
)
}
AppBody
#Composable
fun AppBody(
viewModel: MainViewModel,
navController: NavHostController,
setTitle: (String) -> Unit,
)
{
NavHost(
navController,
startDestination = LESSON_SCREEN_ROUTE
) {
composable(route = LESSON_SCREEN_ROUTE) {
LessonBody(
viewModel = viewModel,
setTitle = setTitle
)
}
composable(
route = STUDENT_SCREEN_ROUTE
) {
StudentBody(
viewModel = viewModel,
setTitle = setTitle
)
}
}
}
In the ViewModel I use this pattern to navigate:
private val _navigateToStudents: MutableState<Boolean> = mutableStateOf(false)
val navigateToStudents: State<Boolean> = _navigateToStudents
fun studentsNavigated()
{
// here we can add any logic after doing navigation
_navigateToStudents.value = false
}
So when I want to navigate to the next fragment I just set _navigateToStudents.value = true
I was just trying to achieve exactly the same today. I think this code will answer both questions:
#Composable
fun NavigationScaffold(
title: String? = null,
navController: NavController? = null,
bodyContent: #Composable (PaddingValues) -> Unit
) {
val navigationIcon: (#Composable () -> Unit)? =
if (navController?.previousBackStackEntry != null) {
{
IconButton(onClick = {
navController.popBackStack()
}) {
Icon(Icons.Filled.ArrowBack)
}
}
} else {
// this can be null or another component
// If null, the navigationIcon won't be rendered at all
null
}
Scaffold(
topBar = {
TopAppBar(
title = {
Text(text = title.orEmpty())
},
navigationIcon = navigationIcon,
)
},
bodyContent = bodyContent
)
}
As you can see, you can provide a title as a String so you don't have to worry about passing a Text composable.
This Scaffold is the base of all my screens and to use it a simply have to write something like that:
#Composable
fun Home(navController: NavController) {
NavigationScaffold(
title = "Home",
navController = navController
) {
// Screen's content...
}
}

Categories

Resources