`LaunchedEffect` doesn't re-render when the mutabl...
# compose
v
LaunchedEffect
doesn't re-render when the mutable state changes inside the ViewModel
I am trying to redirect the user to the
Home
screen when the user enters their credentials and presses the login button inside the
Login
composable. The
LaunchedEffect
with this redirect logic is located in the main
MyApp
composable wrapper.
MyApp
composable wrapper that is run from within `MainActivity`:
Copy code
@Composable
fun MyApp(sessionViewModel: SessionViewModel = hiltViewModel()) {
    val navController = rememberNavController()
    MyAppNavHost(navController)

    val sessionStatus by sessionViewModel.sessionStatus

    LaunchedEffect(sessionStatus) {
        println("RERENDERED") // gets printed only once
        if (!sessionStatus) {
            sessionViewModel.checkSession()
        }

        if (sessionStatus) {
            navController.navigate(Home) {
                popUpTo(Login) { inclusive = true }
            }
        } else {
            navController.navigate(route = Login)  {
                popUpTo(Home) { inclusive = true }
            }
        }
    }

}
Session ViewModel
that contains the state variables and the function to update them:
Copy code
@HiltViewModel
class SessionViewModel @Inject constructor(private val sessionManager: SessionManager) : ViewModel() {

    private val _sessionStatus : MutableState<Boolean> = mutableStateOf(false)
    var sessionStatus: MutableState<Boolean> = _sessionStatus


    fun createSession(credentials: Credentials) {
        viewModelScope.launch {

            _sessionStatus.value = sessionCreated.isSuccess


            _isAuthenticating.value = false
        }
    }
}
The
LoginScreen
composable contains the input fields and a button that, when pressed, triggers
createSession
in the viewmodel. The login itself is successful, however the
LaunchedEffect
inside
MyApp
does not trigger a rerender despite the
_sessionStatus
value gets updated inside the session viewmodel. What am I doing wrong? Could it be that it is because the button press originates inside a
LoginScreen
composable and not inside the
MyApp
composable and the latter cannot detect the sessionStatus change that this press triggers in the viewmodel?
Or maybe this is creating a separate
SessionViewModel
instance that is tied to the
LoginScreen
and therefore not visible
MyApp
?
Copy code
fun NavGraphBuilder.loginScreenGraph() {
    composable<Login> {
        val sessionViewModel: SessionViewModel = hiltViewModel()
        LoginScreen(onLogin = { credentials -> sessionViewModel.createSession(credentials) }, isAuthenticating = sessionViewModel.isAuthenticating.value)
    }
}


@Serializable
object Login
i
Yep, you're creating two separate ViewModels, one scoped to the containing activity and one to the individual destination
If you capture the
LocalViewModelStoreOwner.current
at the MyApp level, you can pass that down to your screen and pass that to
hiltViewModel
z
sessionStatus should also be a val if it has a State type - otherwise if you change the state object it refers to nothing will get the update
i
Yeah, that too 😅
v
@Ian Lake could you kindly provide a quick pseudo code snippet/ example, please?
@Zach Klippenstein (he/him) [MOD] Do you mean simply to do this?
Copy code
var sessionStatus -> val sessionStatus
👍🏻 1
i
Which part are you confused about? Creating a variable at the MyApp level from the ViewModelStoreOwner at that level? Passing that variable to your NavGraphBuilder extension? Or updating the hiltViewModel to use it?
v
The latter 2 questions
the Navgraph thing and the hiltViewmodel update
come to think about it, not sure what creating the variable from the ViewModelStoreOwner means either
i
val owner = LocalViewModelStoreOwner.current
,
fun NavGraphBuilder.loginScreenGraph(owner: ViewModelStoreOwner)
, and
hiltViewModel(owner)
Just a regular old variable you pass down
The scoping page of ViewModels (switch to the Compose tab for code snippets) might be helpful reading: https://developer.android.com/topic/libraries/architecture/viewmodel/viewmodel-apis
v
@Ian Lake Thank you. I have updated the navgraph builder. is val owner = LocalViewModelStoreOwner.current supposed to go inside the MyApp composable?
i
Yeah, that's how you get the outer scope
v
btw, I have separated my navhost out of the MyApp composable like so:
Copy code
@Composable
fun MyAppNavHost(navController: NavHostController) {

    NavHost(navController = navController, startDestination = Login) {
        homeScreenGraph()
        loginScreenGraph()
    }

}
so I am assuming, I just add the owner as a param inside the
MyAppNavHost
function and pass it down?
i
Sure. Technically, both MyApp and MyAppNavHost, since they are both outside the NavHost, will have the same
LocalViewModelStoreOwner
, so either spot is fine for creating the variable
v
btw, val owner = LocalViewModelStoreOwner.current is potentially null?
i
Ah, yeah there are cases like Compose in system overlay windows, etc. where you might not have one, but if you're in an activity, you can just
!!
or
checkNotNull()
around it let the compiler know you know it will always be available
v
Thank you, it is working now. I will make sure to read the article that you linked, so I get a better understanding. One last quick question, if I may: Could you explain a bit more the following, please:
since they are both outside the NavHost, will have the same
LocalViewModelStoreOwner
i
Ah, read that doc first
v
Ok, will do. uh oh, sorry almost forgot: Is
val owner = LocalViewModelStoreOwner.current!!
the conventional way to go about it? Or is there a way to actually check for null and somehow troubleshoot it?
in this scenario specifically
i
That's the
checkNotNull()
option
v
I see, reading about it, will crash the app. But I guess the app would crash in both cases, anyway. Thank you very much for your patience, help and information Ian, much appreciated!