'Jetpack compose navigation with viewModel

I'm trying to use Jetpack Compose navigation inside viewModel. when navigation gets triggered nothing happens. here's my approach:

I define NavigationDestination. In my case, I have four screen: welcome, sign in, sign up, and survey

interface NavigationDestination {
    val route: String
}

sealed class Screen(override val route: String) : NavigationDestination {

    object WelcomeScreen : Screen("welcome")
    object SignInScreen : Screen("signIn")
    object SignUpScreen : Screen("signUp")
    object SurveyScreen : Screen("survey")

}

the Navigator, which exposes the current destination with the default screen of WelcomeScreen.

class Navigator {

    var destination: MutableStateFlow<NavigationDestination> = MutableStateFlow(Screen.WelcomeScreen)

    fun navigate(destination: NavigationDestination) {
        this.destination.value = destination
    }

}

In the main composable, I obtain the NavHostController and listen for changes to the Navigator.

@Composable
fun JetSurvey0App() {
    val welcomeViewModel: WelcomeViewModel = viewModel(factory = WelcomeViewModelFactory())
    val navigator = Navigator()
    val navController = rememberNavController()
    val destination by navigator.destination.collectAsState()

    LaunchedEffect(destination) {
        if (navController.currentDestination?.route != destination.route) {
            navController.navigate(destination.route)
        }
    }

    NavHost(navController = navController, startDestination = navigator.destination.value.route) {
        composable(Screen.WelcomeScreen.route) {
            WelcomeScreen(
                onEvent = { event ->
                    when (event) {
                        is WelcomeEvent.SignInSignUp -> welcomeViewModel.handleContinue(event.email)
                        WelcomeEvent.SignInAsGuest -> welcomeViewModel.signInAsGuest()
                    }
                }
            )
        }
        composable(Screen.SignUpScreen.route) {
            SignUp()
        }
        composable(Screen.SignInScreen.route) {
            SignIn()
        }
        composable(Screen.SurveyScreen.route) {
            SurveyQuestionsScreen()
        }


    }
}

here in WelcomeViewModel I perform decoupled navigation by invoking the Navigator like so.

class WelcomeViewModel(
    private val userRepository: UserRepository,
    private val navigator: Navigator
    ) : ViewModel() {

    fun handleContinue(email: String) {
        if (userRepository.isKnownUserEmail(email)) {
            viewModelScope.launch {
              navigator.navigate(Screen.SignInScreen)
            }
        } else {
          viewModelScope.launch {
              navigator.navigate(Screen.SignUpScreen)
          }
        }
    }

    fun signInAsGuest() {
        viewModelScope.launch {
            navigator.navigate(Screen.SurveyScreen)
        }
        userRepository.signInAsGuest()
    }


}

class WelcomeViewModelFactory : ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(WelcomeViewModel::class.java)) {
            return WelcomeViewModel(UserRepository, Navigator()) as T
        }
        throw IllegalArgumentException("Unknown ViewModel class")
    }
}


Solution 1:[1]

collectAsState triggers recomposition each time you emit a new value to the flow. It means, that JetSurvey0App will be re-called.

You're trying to navigate using navigator.destination, but you're creating a new object on each recomposition:

val navigator = Navigator()
val destination by navigator.destination.collectAsState()

You can make your WelcomeViewModel.navigator public instead of private and collect its destination - as you change the state of this particular object.

Read more about recompositions in Compose Mental Model.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Pylyp Dukhov