I want to have navigation that looks like this:
-I have three screens: "Login", "Registration", and "Account recovery";
-from each screen, I can navigate to any other;
-when I navigate from the "Login" screen to "Registration" and then return to the "Login" (clicking the button "Go to login"), I want to have the same screen as at the beginning, not a new one.
Now, each time, when I go back to "Login", I get a new screen :(
My NavHost:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = Screens.Login.route) {
composable(route = Screens.Recovery.route) {
RecoveryScreen(navController = navController)
}
composable(route = Screens.Login.route) {
LoginScreen(navController = navController)
}
composable(route = Screens.Registration.route) {
RegistrationScreen(navController = navController)
}
}
Guide me which way to dig?
I am late, but it may help someone check Jetnews sample app in GitHub,
navigate like this
navController.navigate(JetnewsDestinations.HOME_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
}
this avoid multiple back stack also restore state of previous opened destination
How do you know that it is a new screen? Let me guess, you must be looking at some state of the screen, for example, filled textfields would be empty, scrolled lists would be reset, checkboxes would be reset or something to this effect. You see it does not matter whether you are calling the stuff from the backstack. The thing is, the moment a Composable is no longer visible on screen, it is destroyed, resetting all the state to the default values. That is, it will always be recomposed upon navigation request. What you need here is to store all that state inside a viewmodel. Then, reference the state from the viewmodel itself. Create state variables in the viewmodel with default values, then always reference from the viewmodel itself. This way, upon recomposition, the data would be fetched from the vm, and it will still be the correct data since the vm is not destroyed.
Related
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
When I start up my app I want it to be able to restore the user's last session if it wasn't completed. How do I restore the navigation backstack while using Jetpack Compose?
My application presents educational sessions, with pages being generated dynamically. Back/up navigation works fine.
#Composable
fun SessionScreenLayout(
pageIndex: Int,
mainViewModel: MainViewModel,
) {
...
}
If a session is not completed when the app shuts down then I manually store that session and I want to be able to restore it when the user starts up again. Importantly I want back navigation to navigate sequentially backwards through my Session pages, after I have restored a session
I've tried populating the backstack but creating a new NavBackStackEntry seems to have 2 options. First doesn't work because the new NavBackStackEntry has the same id as the currentBackStackEntry. After populating the back stack I navigate to the last page and that works fine. but if I navigate backwards the page doesn't recompose properly:
val entry = NavBackStackEntry(
entry = currentBackStackEntry!!,
arguments = Bundle().apply {
...
}
)
navController.backQueue.addLast(entry)
navController.navigate(Screen.SessionScreen.withIndex(userSession.highestPageIndexViewed))
Else have tried using NavBackStackEntry.create():
val entry = NavBackStackEntry.create(
hostLifecycleState = ?,
viewModelStoreProvider = ?,
...
)
navController.backQueue.addLast(entry)
navController.navigate(Screen.SessionScreen.withIndex(userSession.highestPageIndexViewed))
With this I need the "viewModel" and "lifecycleOwner" private fields values from the navController for the constructor AFAIK. Without those I'm getting an exception. Again the last navigation works, but when I go back:
java.lang.IllegalStateException: You must call setViewModelStore() on your NavHostController before accessing the ViewModelStore of a navigation graph.
How do I get this working?
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.
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)
}
I have 2 navigation files, and in my Activity, 2 fragments. One of the navigations is always shown inside one of the fragments, but I show the other one only when I need it.
The way they're drawn is the always showing fragment is inside a relativeLayout, and the other fragment is inside the same relativeLayout with it's visibility set as gone. When I need the second navigation, I set the visibility to visible and when I don't need it, I set it to gone again.Visually this works well, but what I want to accomplish is that when I don't want the second navigation, I want to completely kill it and redraw it the next time I need it.
What I've done so far was to get a hold of the NavHostFragment used to start the navigation, and when I dont need it anymore, call popBackStack() on it's navController, but it doesn't work:
val navHost: NavHostFragment? = null
fun createSecondNav() {
navHostLogin = NavHostFragment.create(R.navigation.navigation_second)
theFragment.visibility = View.VISIBLE
supportFragmentManager.beginTransaction()
.replace(R.id.theFragment, navHostLogin!!)
.commit()
}
fun killSecondNav() {
theFragment.visibility = View.GONE
navHostLogin?.navController?.popBackStack() // returns false
navHostLogin = null
}
So how can I completely kill the fragments created by the second navHost?
NavController maintains it's own back-stack, independent form the FragmentManager back-stack.
And popBackStack() without arguments only pops that back-stack once:
Attempts to pop the controller's back stack. Analogous to when the user presses the system Back button when the associated navigation host has focus.
While popBackStack(int destinationId, boolean inclusive) reads:
Attempts to pop the controller's back stack back to a specific destination.
destinationId int: The topmost destination to retain
inclusive boolean: Whether the given destination should also be popped.
So this should be:
navController.popBackStack(R.id.startDestination, true)
I'd wonder why even using two NavController, because one can set the graph at run-time with setGraph(NavGraph graph, Bundle startDestinationArgs):
Sets the navigation graph to the specified graph.
Any current navigation graph data (including back stack) will be replaced.