androidkotlinnavigationandroid-jetpack-compose

Jetpack Compose navigation crashes app after orientation change


I use Jetpack Compose Navigation in my app.

I have 2 screens: A, B.

The navigation graph looks like: A⟷B

Where → is navigate(route), and ← is navigateUp() or hardware back button.

I use ViewModel for screen A:

class MyViewModel(private val _openScreenB: () -> Unit) : ViewModel() {
    fun openScreenB() {
        _openScreenB()
    }
}

NavContoller and NavHost:

setContent {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        ...
    ) {
        composable("ScreenA") {
            val viewModel: MyViewModel = viewModel(
                factory = MyViewModelFactory(
                    openScreenB = {
                        navController.navigate("ScreenB") {
                            launchSingleTop = true
                        }
                    }
                )
            )
            ScreenA(
                navigateToScreenB = {
                    viewModel.openScreenB()
                }
            )
        }
        composable("ScreenB") {
            ScreenB(
                navigateBack = {
                    navController.navigateUp()
                }
            )
        }
    }
}

Everything works fine until activity recreation (orientation change or switch light/dark mode). After recreation, line val navController = rememberNavController() is called again. The NavController recreates, and now navigateUp() does nothing, hardware back works, and navigate(route) crashes app with error.

java.lang.IllegalStateException: State must be at least CREATED to move to DESTROYED, but was INITIALIZED in component NavBackStackEntry(2d8f0727-c13b-4635-ac94-f279608c3cfc) destination=Destination(0x67884f5a) route=Editor
    at androidx.lifecycle.LifecycleRegistry.moveToState(LifecycleRegistry.jvm.kt:131)
    at androidx.lifecycle.LifecycleRegistry.setCurrentState(LifecycleRegistry.jvm.kt:107)
    at androidx.navigation.NavBackStackEntry.updateState(NavBackStackEntry.kt:186)
    at androidx.navigation.NavBackStackEntry.setMaxLifecycle(NavBackStackEntry.kt:159)
    at androidx.navigation.NavController.updateBackStackLifecycle$navigation_runtime_release(NavController.kt:1100)
    at androidx.navigation.NavController.dispatchOnDestinationChanged(NavController.kt:996)
    at androidx.navigation.NavController.navigate(NavController.kt:1882)
    at androidx.navigation.NavController.navigate(NavController.kt:1817)
    at androidx.navigation.NavController.navigate(NavController.kt:2225)
    at androidx.navigation.NavController.navigate$default(NavController.kt:2220)
    at ...

I tried to navigate with saveState = true, restoreState = true, and launchSingleTop = true properties, but this doesn't fix the problem. I also tried to use navController saveState() in onSaveInstanceState and restoreState(bundle) after rememberNavController, but it also doesn't help.


Solution

  • At the first version of my question I don't know that problem was in my ViewModel. I tried to create a minimal reproducible example without ViewModel:

    setContent {
        val navController = rememberNavController()
        NavHost(
            navController = navController,
            ...
        ) {
            composable("ScreenA") {
                ScreenA(
                    navigateToScreenB = {
                        navController.navigate("ScreenB") {
                            launchSingleTop = true
                        }
                    }
                )
            }
            composable("ScreenB") {
                ScreenB(
                    navigateBack = {
                        navController.navigateUp()
                    }
                )
            }
        }
    }
    

    When I created it, I realised all was working perfectly. So I guess that the problem was in my ViewModel.

    I was passing navigation lambdas to ViewModel constructor for a long time. It caused crashes. I think that saving a navController as a ViewModel property breaks lifecycle of the navController.

    Removing all the lambdas with navController from ViewModel constructor solved my problem

    P.S. I passed that navigation lambdas directly to composables