https://kotlinlang.org logo
#compose
Title
# compose
p

Pablo

03/06/2024, 12:34 PM
If you have a single activity, is it a good practice to store in the viewmodel of that activity the navigation route of the navhost that manages the navigation on the application? I mean to have inside that viewmodel a "appuistate" state var with the current route. The app haves various screens that will be switched with a drawer, bottom bar or rail navigating with the navhost. I need to store the current route somewhere. I think the viewmodel of the single activity should be the best place to store it, as it is the higher place to do it, and as the navigation is common for the entire app. My main concern about this is that a viewmodel of the activity will be used at the same time as a viewmodel of the screens (routes) of the app. I don't know if this is a good practice in Compose.
s

Stylianos Gakis

03/06/2024, 1:08 PM
https://kotlinlang.slack.com/archives/CJLTWPH7S/p1709728313088939?thread_ts=1709674897.162639&cid=CJLTWPH7S Your NavController has a different lifecycle from your ViewModel. If you put it on your ViewModel you'll be creating memory leaks. You need to create your NavController using this https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:navigation/navigation-compose/src/main/java/androidx/navigation/compose/NavHostController.kt;l=56?q=RememberNavController to properly survive configuration changes, which is done in a composable context which you don't have in your ViewModel.
p

Pablo

03/06/2024, 2:41 PM
I don't mean putting the navcontroller in the viewmodel, just the route. I mean simply a var of the enum type that the navcontroller uses to know which screen is selected. Just the route. In a AppUiState state variable stored in the viewcontroller.
The viewmodel of the main and single activity whould contain just this:
Copy code
private val _uiState = MutableStateFlow(uiState())
val uiState: StateFlow<UiState> = _uiState

fun updateCurrentTab(selectedTab: Screen) {
    _iiState.update {
        it.copy(
            selectedTab = selectedTab
        )
    }
}
s

Stylianos Gakis

03/06/2024, 2:59 PM
You're introducing a point of failure if for some reason this variable then shows one thing, but in reality you are in another route. Aka not using a single source of truth for your destination. Do it at your own risk.
p

Pablo

03/06/2024, 3:13 PM
please, can you help me understanding how can that situation happens? will help me to improve myself, because at this point I don't know how can that happens
s

Stylianos Gakis

03/06/2024, 3:29 PM
It depends when you will call
updateCurrentTab
, it’s up to you I suppose.
p

Pablo

03/06/2024, 5:53 PM
I call updateCurrentTab just when an user press the button on the navigation rail, the drawer or the bottom navigation bar
how that can produce a point of failure?
why is not that a single source of destination?
as I can see, the only source of truth is exactly that variable stored in uistate inside the viewmodel of the main activity
which other sources of truth do exist?
s

Stylianos Gakis

03/06/2024, 6:32 PM
By definition, your truth regarding what your NavHost shows is inside the NavController. This field that you need to remember updating is another source of truth that can absolutely be lying compared to the real destination. But if you feel confident keeping those two in sync you are absolutely free to do so 😅
p

Pablo

03/06/2024, 8:44 PM
whould that dual source of truth possibility be avoided if I use a remember mutablestateof with the navcontroller inside and I get the current destination from it?
and what if I simply store the navcontroller in the uistate of the viewmodel and get the current destination from it?
I mean, this:
Copy code
private val _uiState = MutableStateFlow(uiState())
val uiState: StateFlow<UiState> = _uiState
having this inside UiState:
Copy code
data class UiState (
    val navController: NavHostController
)
s

Stylianos Gakis

03/06/2024, 8:52 PM
We've done a full loop. Now you're storing the NavController in your ViewModel, which introduces the other issues I was talking about before.
p

Pablo

03/06/2024, 8:58 PM
but I'm losing something, the idea is that the navcontroller survives to configuration changes, to recompositions etc...
if the viewmodel survives that, why can it be a problem?
is not the same that having it in a mutablestate remember var?
s

Stylianos Gakis

03/06/2024, 8:58 PM
Yes exactly, the ViewModel will, the NavController won't. So the VM will be referencing the old NavController
p

Pablo

03/06/2024, 8:59 PM
ouch
ok
and having it in a mutablestate remember var without a view model will force the navcontroller to survive? (solving the issue)
s

Stylianos Gakis

03/06/2024, 9:04 PM
This message https://kotlinlang.slack.com/archives/CJLTWPH7S/p1709730511976179?thread_ts=1709728460.970259&amp;cid=CJLTWPH7S is the one that links to the code which ensures that when doing rememberNavController you will get the right behavior of it being restored well when there is a configuration change. You don't get back the same instance, but the new instance you get back will have the right restored state
p

Pablo

03/06/2024, 9:25 PM
I understand, thank you, but I see this as extremely complex for a very small feature
I mean, this was massively simple before compose, and it seems I'm overprogramming to simply manage current tab in a screen
I hope there is a simpler way to achieve this, I'm not very happy with this approach, not confortable for me
I noticed something, in all the codelabs I did, I never used your approach, but.. even better, I never stored the current screen in any state variable
in the codelabs, the navcontroller was simply initialized like this in the parameter of the main screen composable:
Copy code
@Composable
fun LunchTrayApp(
    viewModel: OrderViewModel = viewModel(),
    navController: NavHostController = rememberNavController()
) {
and it works!!! if I change device orientation producing a configuration change, the nav controller paints the current screen
so, is not necessary then to store it in a state variable? can it simply be obtained with that compose built int method called rememberNavController() to keep always the current screen remembered and without any issues?