How to clear backstack when browsing Jetpack Compose?
Asked Answered
R

3

5

I'm implementing the Logout action in an application. I want that when user clicks Logout, go to Login. When the user takes this path: Login -> Home -> Settings (where he clicks on Logout) -> Login, when I press back, the app goes to the background and closes, which is the behavior I want.

However, when the user takes this route: Login -> Home -> ScreenOne -> ScreenTwo -> Home -> ScreenOne -> Settings (where you click on Logout) -> Login, when you press back, it goes back to Settings and if you press it again it goes to ScreenOne and so on .

That's the way I do the navigation to Login when I click on Logout:

navController.navigate(NavigationItem.Login.route) {
            popUpTo(NavigationItem.Login.route) {
                inclusive = true
            }
        }

Note: Already tried Navigation.Home.route as parameter on popUpTo.

I don't know if is related, but that's the way I do the navigation between Home -> ScreenOne -> ScreenTwo -> Home -> ScreenOne:

navController.navigate(item.route) {
                    navController.graph.startDestinationRoute?.let { route ->
                        popUpTo(route = route) {
                            saveState = true
                        }
                    }

                    launchSingleTop = true
                    restoreState = true
                }

Does anyone knows how I can clear the back stack or guarantee that, in the second behavior, when I am on Login screen after Logout and I press "Back", the app goes to second plan?

EDIT: Added NavHost structure.

@Composable
@ExperimentalFoundationApi
@ExperimentalComposeUiApi
@ExperimentalMaterialApi
fun Navigation(navController: NavHostController, updateBottomBarVisibility: (Boolean) -> Unit) {
    NavHost(
        navController = navController,
        startDestination = NavigationItem.Login.route
    ) {
        composable(route = NavigationItem.Login.route) {
            LoginScreen(navController)
        }

        composable(route = NavigationItem.Events.route) {
            EventsScreen(updateBottomBarVisibility, navController)
        }

        composable(route = NavigationItem.Home.route) {
            HomeScreen(updateBottomBarVisibility, navController)
        }

        composable(route = NavigationItem.Prizes.route) {
            PrizesScreen(updateBottomBarVisibility, navController)
        }

        composable(route = NavigationItem.Account.route) {
            AccountScreen(navController)
        }
    }
}
Rager answered 11/11, 2022 at 15:51 Comment(2)
do you have only one NavHost?Woof
@Woof Yes, only one NavHost.Rager
W
8

When you are in settings logout:

navController.popBackStack(NavigationItem.Login.route, inclusive = false, saveState = false)

is not necessary to save the state since once the use is out has to login again with new updated data.

At this point if the user click back the app should go in foreground.

If this doesn't work is possible that Login screen is not on the backstack. I would try to add a route to the NavHost and popBack to it with exclusion and save state off.

Otherwise might be that you are invoking navigate method more times thank expected

fun Navigation(navController: NavHostController, updateBottomBarVisibility: (Boolean) -> Unit) {
    NavHost(
        route = "nav_host_route"
        navController = navController,
        startDestination = NavigationItem.Login.route
    ) {}

//navigate to top destinations like HOME
navController.navigate(item.route) {
                    popUpTo(route = "nav_host_route") {
                        inclusive = false
                        saveState = true
                    }
                  
                    launchSingleTop = true
                    restoreState = true
                }

log out:

navController.navigate(LogIn){ 
    popUpTo("nav_host_route"){
        saveState = false
        inclusive = false
    }
    restoreState = false
    launchSingleTop = true
}
Woof answered 12/11, 2022 at 9:57 Comment(6)
I was not able to get this to work, when I clicked on the Logout button, there was no navigation to Login and, even adding the navController.navigate(NavigationItem.Login.route), the app performed this navigation, but when I pressed on Back , the app returned to the Settings page instead of being closed and placed in the background.Rager
Added NavHost structure to the question. AccountScreen is where I have my Logout button. Events, Home & Prizes are my screens where the navigation is done using a BottomNavigationBar.Rager
@Rager add a route to the NavHost and use it as popUp destination, it should reload Login if it is not on the backstackWoof
Of course Login is not in my backstack, because when I navigate from Login to Home, I popUp(Login.route) { inclusive = true } or else, if I was on Home and the user presses "Back" , it would go back to Login, which it's not supposed to, because he's already Login, he can't go back there unless he Logout.Rager
@Rager Ok, because there was popTo(Login), so if there is no Login pop to Home inclusive or assign a route to the host and pop to the host exclusiveWoof
I already tried this, the problem is that I have more than one Home in the backstack and it will pop for one, but there are others there and therefore it won't work, because when I do back it will still go back inside from the app. I think this happens because of the way I do the navigation between the BottomNavigation screens (second code block of the question), where there must be something wrong. When you say exclusive, do you mean inclusive = false?Rager
H
0

To close the app on backpress you can use the following piece of code in your "Log In" screen composable:

// Getting your activity in a composable function
val activity = (LocalContext.current as? Activity)

// Everything put inside this block will be done on each system backpress
BackHandler {
    activity?.finish()
}
Honorary answered 11/11, 2022 at 23:10 Comment(9)
This worked, but I have two questions here. 1) Is it the best way to do this? 2) When I click to see the apps that are in the background and I click mine to reopen it, it opens the app as if I was opening it again, that is, it shows me the splash screen, instead of the Login, because the Activity was killed. Is there no way to do this with the navController without killing the activity?Rager
Added NavHost structure to the question. AccountScreen is where I have my Logout button. Events, Home & Prizes are my screens where the navigation is done using a BottomNavigationBar.Rager
1) In terms of programatically closing an app, I am not aware of any other way in Jetpack Compose. In terms of app design that is up to you! 2) The splash screen will show every time you open an app (depending on its implementation of course), you can add logic on launch which will show your login screen if the user has logged out. Compose navigation has not been great to use so far (as you can see =]). I've built my own system instead which has been wonderful and I'd highly recommend it if you are able to too. Hopefully I was able to answer your questions, cheers!Honorary
1) I don't see that as a bad way, but I don't know, I look to that solution and I don't feel that's the best way to do it, but maybe it is. 2) So far, this is the biggest problem I've had with Compose Navigation so far, but I never found Navigation to be easy to use for this particular case, even with XML. Anyway, what system did you implement and use? Can I have access to it?Rager
1) If it works with no issues it's good in my eyes =) 2) I made an object with a MutableStateList<Screen> (screen is an enum class of my app's screens) and methods to manipulate it, and always display the last (or top) screen in it. It seems too simple to be a good reliable solution so I am still wary of it and test it as often as possible, but so far so good. No bugs or performance issues, and it allows you to add animations between screens as well!Honorary
Interesting solution! Do you have any repo or post or something where I can find it and check out the code?Rager
Unfortunately no :/, it's a very simple implementation though, you just need a Kotlin Object to act like your Navigation Stack/NavHost and use it in your app as you please!Honorary
I was able to solve my issue with the solution suggested by @WoofRager
This worked for me. I am using this when user clicks back button on my First screen which is LoginScreen. If I don't finish the activity, the entered input fields remains same without cleared. I am calling as below : val activity = (LocalContext.current as? Activity) BackHandler(enabled = true) { userEmail = "" password = "" activity?.finish() }Posthorse
V
0

I know you might have figured it out, but for someone else still in this situation:

In your NavHost set startDestination to NavigationItem.Home.route. This will ensure that at any given instance, cold boot or warm boot of your application will launch your Home screen, and when a user clicks the back button, the app will exit.

Now to solve your problem, you can have a condition at the start of your home screen that checks whether the user is logged in or not. If logged in, proceed (i.e., do nothing, the back button will be taken care of by the above NavHost), else, pop the back stack then navigate the user to Login.

navController.popBackStack()
navController.navigateSingleTopTo(NavigationItem.Login.route)

The navigateSingleTopTo is as below:

fun NavHostController.navigateSingleTopTo(route: String) =
    this.navigate(route) {
        // Ensures there will be at most one copy of a given destination on
        // the top of the back stack i.e. re-tapping the same tab multiple
        // times doesn't launch multiple copies of the same destination
        launchSingleTop = true
    }

Since you popped the back stack, clicking on the back button here will always kill the app (for a non-logged in user).

For Logout, since you want the user to be taken to the Login screen irrespective of their current destination, pop up to your Start Destination (in your case Home screen), then navigate to Login screen, as below:

navController.navigate(NavigationItem.Login.route) {
                            popUpTo(navController.graph.findStartDestination().id) {
                                inclusive = true
                            }
                        }

This will mean at that particular moment when a user clicks the back button they kill the app.

You will lastly need to update the condition you were checking in the Home screen so that the app can now know user has logged out. This means if the user perform a warm boot (i.e., kills and opens the app), Login will be launched, upon which clicking the back button will kill the app, as expected.

Let me know if it helps your case.

Happy coding!

Velarde answered 24/12, 2023 at 20:0 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.