Jetpack Compose navigation NavController.popBackStack() not working properly - android

When working with Compose Navigation and calling NavController.popBackStack() multiple times on the first shown Composable (startDestination) the backnavigation does not work anymore. For example when navigating to another Composable from this point on and then calling popBackStack does not have an effect.

For some Reason the size of the NavController.backQueue is at least 2 even though it's supposed to only show one Composable. If popping the backstack lower than that, the navigation does not seem to work anymore. (I don't know why)
Therefore I wrote the following simple extension function which prevents popping the BackQueue lower than 2:
fun NavController.navigateBack(onIsLastComposable: () -> Unit = {}) {
if (backQueue.size > 2) {
popBackStack()
} else {
onIsLastComposable()
}
}
You can use it like this:
val navController = rememberNavController()
...
navController.navigateBack {
//do smth when Composable was last one on BackStack
}

Related

Compose navigation - state not always restored

I'm having a problem using compose navigation and bottom navigation bar. Using these navOptions, the state is not always restored when going back to a previously selected tab in the bottom navigation.
val topLevelNavOptions = navOptions {
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
When clicking through the screens sporadically, I lose the state and get new instances of the viewmodels injected using hilt.
Similar behaviour be observed in the nowinandroid app.
Clone the repository and add the following to InterestsViewModel.kt:
override fun onCleared() {
super.onCleared()
Log.d("asd", "onCleared")
}
Then proceed to sporadically click through the screens using the bottom navigation. The log statement above will be printed... sometimes
Does anyone know why the state is lost/reset and how to prevent that from happening?
EDIT
I believe this is a bug in navigation compose. Reported here https://issuetracker.google.com/issues/265838050

Share composable in nav host

I have a NavHost which hosts multiple composables from the main screen to the login screen as show below:
#Composable
fun Navigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Main.route) {
composable(route = Screen.Main.route) {
StatusBar()
Main()
}
composable(route = Screen.Login.route) {
StatusBar()
Login()
}
}
}
You see how the Status bar is in both Main and Login composables I was just wondering if it would be possible to define it in one place so it can be used across all composables?
To keep your code clean I would recommend using the StatusBar in Main and Login. Because at the end the StatusBar and its state belongs to the screen and not to the NavHost.
The problem is that your screen is the component which owns all the information that the StatusBar needs (even though there are no parameters; this is theoretical representation). So it makes sense that the Screen owns its sate.
The composable of the NavHost on the otherside only knows the curren screen, but none of its state unless you state hoist it in a very ugly way. So it makes sense that it only holds that state it is best in managing.
Because of the fact that you already have put the StatusBar into an composable you also don't have a problem with code duplication.

Compose-Navigation: Remove previous composable from stack before navigating

I'm using compose-navigation(alpha09) to handle the navigation between composables
I want to remove the Splash screen when moving to the next destination (I don't want the back pressed to get back to Splash)
Following attempts did not work as expected:
navHostController.navigate(Route.login.id) {
navHostController.graph.clear()
}
navHostController.navigate(Route.login.id)
navHostController.graph.clear()
val currentDest = navHostController.currentDestination
navHostController.navigate(Route.login.id)
if (currentDest != null) {
navHostController.graph.remove(currentDest)
}
So how can I remove the Splash screen and then move to next?
In Jetpack Compose 1.0.0-rc01 to navigate and remove previous Composable from back stack You can use:
navController.navigate(Screens.Login.name) {
popUpTo(Screens.Splash.name) {
inclusive = true
}
}
The above code will navigate from the Splash screen to Login and will pop everything up, including the Splash screen.
Navigate to a composable - docs
For v1.0.0-alpha09 (And 1.0 stable)
Using popUpTo(0) you can clear the stack before navigating to the next destination. So:
navHostController.navigate(Route.login.id) {
// popUpTo = 0 // DEPRECATED
popUpTo(0)
}
For a consistent reusable function that does not need to be aware of the current route, use this NavOptionsBuilder extension function
fun NavOptionsBuilder.popUpToTop(navController: NavController) {
popUpTo(navController.currentBackStackEntry?.destination?.route ?: return) {
inclusive = true
}
}
^ Similar to other answers, it popUpTo the current route, but rather than needing to name the specific current route, it instead gets it from the backstack entry.
Now you can use it like so:
navController.navigate(ScreenRoutes.Login.route) { popUpToTop(navController) }
^ That example navigates to Login, and should clear the entire backstack before it.
For clearing all back stack
To remove multiple composable screens from the stack use the below snippet
navController.navigate(ScreenRoutes.Login.route){
popUpTo(navController.graph.findStartDestination().id){
inclusive = true }}
Or To keep Home in back stack
navController.navigate(ScreenRoutes.SelectCourseLayout.route){
popUpTo(ScreenRoutes.Home.route)
}
Apart from screens, back stack contains navigational graphs, and its root is always the first thing in back stack. Our NavHostController contains graph, so by popping its id, you are able to clear your back stack:
popUpTo(navHostController.graph.id)
For more info, here is the detailed explanation https://medium.com/#banmarkovic/jetpack-compose-clear-back-stack-popbackstack-inclusive-explained-14ee73a29df5
To clear the back-stack, you can simply create this Extension function and reuse it wherever applicable.
fun NavHostController.navigateAndClean(route: String) {
navigate(route = route) {
popUpTo(graph.startDestinationId) { inclusive = true }
}
graph.setStartDestination(route)
}
Jetpack Compose v1.0.5
navController.backQueue.removeIf { it.destination.route == "Splash" }
navController.popBackStack()
After so many try, I've found the better way to clear the back stack during the logout scenario. Most of the production app will clear the splash or sign in screen as soon as we navigate to Home screen and there would be a multiple way to land into Home screen as well.
So, we may not know the initial screen to perform the popupTo. If there is a bottom bar, then story would be too difficult as well.
Here is a magic could that work all the scenario
val firstBackStackRoute = navController.backQueue.firstOrNull()?.destination?.route
firstBackStackRoute?.let {
navController.popBackStack(firstBackStackRoute, true)
}

Navigation Controller (Managing Backstack) Jetpack Android

Good day. So I've been working around with NavComponent of Jetpack for Android
I've thought that management of BackStack of fragments had to be implemented there already, well in fact it is there but I have faced an issue.
Here is my structure:
I have and entry Activity
I have a NavHost in the activity
I have Bottom Navigation bar in the Activity
For each Bottom Item I am using separate Fragments to navigate through.
Here is the code for the navigation.
bottomNavigationView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.navigation_home -> {
navController.apply {
navigate(R.id.navigation_home)
}
true
}
R.id.navigation_dashboard -> {
navController.apply {
navigate(R.id.dashboardFragment)
}
true
}
R.id.navigation_notifications -> {
true
}
else -> {
false
}
}
}
Never mind the last item.
So the issue is next.
If I try to switch between home and dashboard multiple times, when I press back then the stack surely will start popping all the items included there. So if I move like 6 times it will take me 12 attempts to actually exit the app.
Currently I couldn't find any source where for example the navigate() method will accept some sort of argument to cash my fragments instead of recreating it each time and adding to the BackStack.
So what kind of approach would you suggest?
If I to manage the BackStack manually on each back button pressed, what's the purpose of NavController at all? Just for creating and FORWARD navigation?
I think I'm missing some source in Android's official docs.
Thank you beforehand.
P.S.
using navController.popBackStack() before calling navigate() surely isn't the correct choice.
According to the documentation here :
NavigationUI can also handle bottom navigation. When a user selects a menu item, the NavController calls onNavDestinationSelected() and automatically updates the selected item in the bottom navigation bar.
to do so you have to give your bottom navigation items an ids as same as the corresponding destination in your navigation graph , and then tie you bottom view to the controller like this :
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
BottomNavigationView bottomNav = findViewById(R.id.bottom_nav);
NavigationUI.setupWithNavController(bottomNav, navController);
Note : from my personal experience , when the startDestination in the graph , that start by default is not currently in back stack (In my case it was the landing page which i pop it out when going to home fragment) then the app act with weird behavior like this . so make sure the start destination is existed in your back stack on should work fine .

NavController currentDestination is null?

I am using NavController to manages app navigation:
findNavController().navigate(action)
I got a few crashes in Crashlytics: I found it is because:
MyFragment {
...
myLiveData.observer(viewLifecycleOwner, Observer) {
findNavController().navigate(myAction) // currentDestination is null ...
})
...
navController.currentDestination? is an optional, When it is null, app crashes with unhandled exception.
Since currentDestination is declared as optional, I guess there must be some legit reason why it could be null, that I don't know. Appreciate in advance for any pointer.
I was experiencing the same issue.
At seemingly random times, the navigate to my destination fragment would crash due to the currentDestination being null.
Similar to the OP, I was triggering the nav through a Flow (not a live data).
Despite collecting the flow with the viewLifecycleOwner, it almost seemed like the fragment wasn't ready to navigate. What I found that fixed the issue was a little surprising. It was how the previous fragment was "popping" itself.
FragA -> FragB
FragB.popBackStack()
FragA -> VERY Quickly re-nav to FragB (null currentDestination == Crash)
However, as a test, I tried using
FragB.popBackStack(fragA.id, false)
And the crashes stopped. The currentDestination was never null again.
This must be a bug in the navComponent library.
My fix was as follows, and is still working (fingers crossed).
Instead of "findNavController.popBackStack()" I use
findNavController().previousBackStackEntry?.let {
findNavController().popBackStack(it.destination.id, false)
} ?: run {
findNavController().popBackStack()
}
Hope that works for someone else also.
edit Left in for posterity.. but.. I was wrong. this didn't fix it afterall. My mistake. Carry on.
Destination represents the node in the NavGraph that's being hosted by the NavHost. NavController just manages the flow. There are few ocasions when NavHost is not showing any destination e.g.:
before you set the NavGraph (because destination represents position in the graph)
when you manually inflate something in the NavHost using transaction (outside of the graph's scope)
If you have multiple graphs in one app (e.g. nested graphs, but can also be independent) you may have one NavController giving main graph destination and a secondary one returning null, etc.
Thanks Stachu, any relationship to fragment viewLifecycle?
In my case, the navigation is triggered from a liveData observer, i.e.,
MyFragment {
...
myLiveData.observer(viewLifecyucleOwner, Observer) {
findNavController().navigate(myAction) // currentDestination is null ...
}
...

Categories

Resources