ViewModel triggered navigation with JetpackCompose
Asked Answered
V

1

6

In Android I often want to navigate is response to state change from a ViewModel. (for example, successful authentication triggers navigation to the user's home screen.)

Is the best practice to trigger navigation from within the ViewModel? Is there an intentional mechanism to trigger navigation within a composable in response to a ViewModel state change?

With Jetpack Compose the process for handling this use case is not obvious. If I try something like the following example navigation will occur, but the destination I navigate to will not behave correctly. I believe this is because the original composable function was not allowed to finish before navigation was invoked.

// Does not behave correctly.
@Composable fun AuthScreen() {
    val screenState = viewModel.screenState.observeAsState()
    if(screenState.value is ScreenState.UserAuthenticated){
        navController.navigate("/gameScreen")
    } else {
        LoginScreen()
    }
}

I do observe the correct behavior if I use LauncedEffect as follows:

// Does behave correctly.
@Composable fun AuthScreen() {
    val screenState = viewModel.screenState.observeAsState()
    if(screenState.value is ScreenState.UserAuthenticated){
        LaunchedEffect(key1 = "test") {
            navController.navigate("$/gameScreen")
        }
    } else {
        LoginScreen()
    }
}

Is this correct? The documentation for LaunchedEffect states the following, but the meaning is not 100% clear to me:

When LaunchedEffect enters the composition it will launch block into the composition's CoroutineContext. The coroutine will be cancelled and re-launched when LaunchedEffect is recomposed with a different key1, key2 or key3. The coroutine will be cancelled when the LaunchedEffect leaves the composition.

Vedetta answered 10/11, 2022 at 14:31 Comment(0)
H
1

This code

// Does not behave correctly.
@Composable fun AuthScreen() {
    val screenState = viewModel.screenState.observeAsState()
    if(screenState.value is ScreenState.UserAuthenticated){
        navController.navigate("/gameScreen")
    } else {
        LoginScreen()
    }
}

which does not behave correctly is most likely causing an issue like this, and one of the ways to solve it is by this

// Does behave correctly.
@Composable fun AuthScreen() {
    val screenState = viewModel.screenState.observeAsState()
    if(screenState.value is ScreenState.UserAuthenticated){
        LaunchedEffect(key1 = "test") {
            navController.navigate("$/gameScreen")
        }
    } else {
        LoginScreen()
    }
}

which behaves correctly, because LaunchedEffect is guaranteed to execute only once per composition assuming its key won't change on the next composition pass, otherwise it will keep executing on every update of its composable scope.

I would suggest considering the "correct" not only based on suggested components but thinking how to avoid navigation pitfalls like the link I provided.

It won't matter if it's coming from a ViewModel or some flow emissions but the idea for a safe navigation in compose (so far as I understand it) is to make sure that the navigation call will only happen in a block that will never re-execute on succeeding re-compositions, which this one also suffers from the first type of code above.

Hinayana answered 10/11, 2022 at 14:56 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.