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.
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 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
}
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.
In my project I have a splash screen, when it is displayed, my app loading some startup data from server, after loading the data shows another screen.
For splash screen I create a ViewModel, but it stays in memory all the time. How to destroy it correctly?
Thank you for help!
#HiltViewModel
class SplashViewModel #Inject constructor (private val repository: Repository) {
....
}
#Composable
fun SplashScreen(vm: SplashViewModel) {
...
}
#Composable
fun Navigate() {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "splash") {
composable("splash") {
SplashScreen(vm = hiltViewModel())
}
composable("main") {
MainScreen(...) // When shows MainScreen, SplashViewModel object still is in memory
}
}
}
Your viewmodel stays in memory because your splash screen is your root destination, and as such it stays always on the stack as the bottom entry.
If you want your splash viewmodel to be automatically destroyed when you leave your splash screen you should pop it from the backstack when you navigate to your main screen, using popUpTo.
Another option you could consider is to make your main screen the root destination and then navigate from that screen to splash if you are starting the app fresh.
Using hiltViewModel and scoping the viewmodel to the nav graph destination as you do will ensure the viewmodel is destroyed when the user leaves that screen, provided it's not in the backstack.
It is not explicitly supported in Android as far as I know. However, you could create a method named onViewModelCleared() inside the viewmodel itself and pass null to all the nullable objects, and something lightweight to non-null objects.
I'm currently new to testing, so I decided to start off with some basic stuff.
I handle all my navigations from a DrawerLayout that is connected to an Activity.
So for my testing I launch an ActivityScenarioRule, create a testNavController object and then I set this testNavController to the current view that handles the navigation (The container fragment).
So the test consists on opening the drawer, clicking on menu item(Will navigate to a fragment) and therefore check if navigated to the fragment.
Then I check if that happened, but the testNavController stays on the same destination which is weird because it performs the click, so I decided to check the navController (The real one inside the activity), and it shows me that navigated to the correct fragment.
Here's the needed code:
#LargeTest
#RunWith(AndroidJUnit4::class)
class MapsActivityTest {
#get:Rule
var activityScenarioRule = ActivityScenarioRule(MapsActivity::class.java)
#Test
fun clickOnDrawerMaps_NavigateToAboutAppFragment() {
//Create TestNavHostController
val testNavController = TestNavHostController(ApplicationProvider.getApplicationContext())
UiThreadStatement.runOnUiThread { // This needed because it throws a exception that method addObserver must be called in main thread
testNavController.setGraph(R.navigation.nav_graph)
}
val scenario = activityScenarioRule.scenario
var navcontroller : NavController? = null
scenario.onActivity {mapsActivity ->
navcontroller = mapsActivity.navController //Get the real navController just to debug
mapsActivity.navController = testNavController //Set the test navController
Navigation.setViewNavController(mapsActivity.binding.containerFragment, testNavController)
}
onView(withId(R.id.drawerLayout)).perform(DrawerActions.open()).check(matches(isOpen()))
onView(withId(R.id.aboutAppFragment)).perform(click())
assertThat(testNavController.currentDestination?.id).isEqualTo(R.id.aboutAppFragment)
}
}
In the example they use a Fragment, which they set the fragment.requireView() on the launch of the fragment, but I think it's exactly the same.
What am I doing wrong here?
When you use ActivityScenario (or ActivityScenarioRule), your activity is brought all the way up to the resumed state before any onActivity calls are made. This means that your real NavController has already been created and used when you call setupWithNavController. This is why your call to setViewNavController() has no effect.
For these types of integration tests (where you have a real NavController), you should not use TestNavHostController.
As per the Test Navigation guide, TestNavHostController is designed for unit tests where you do not have any real NavController at all, such as when testing one fragment in isolation.