by navGraphViewModels creates java.lang.IllegalArgumentException: No destination with ID <destination id> is on the NavController's back stack

Viewed 5586

When a configuration change happens and my Activity and Fragment are recreated because of it, my nav Graph scoped ViewModel is unavailable while the Fragments have already been created again.
It seems like the Fragment gets recreated, before the navGraph does.

I am using this code to initialize my navGraph scoped ViewModel from my Fragment:

private val myViewModel: MyViewModel by navGraphViewModels(R.id.nav_graph_id)

If I try to use myViewModel in the Fragments onViewCreated function, I get a IllegalArgumentException after a Configuration change. The Exception:

java.lang.IllegalArgumentException: No destination with ID <destination id> is on the NavController's back stack

How do I handle this?

I have already checked that my ID isn't used anywhere else.

Edit1:

Here is my activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    
    <androidx.fragment.app.FragmentContainerView
            android:id="@+id/main_nav_host"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:defaultNavHost="true"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:navGraph="@navigation/main_nav_graph" />

</androidx.constraintlayout.widget.ConstraintLayout>

And here is my main_nav_graph.xml:

<navigation
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/main_nav_graph"
        app:startDestination="@id/main_nav_graph_1">
    
    <navigation
            android:id="@+id/main_nav_graph_1"
            android:label="@string/nav_graph_1_label"
            app:startDestination="@id/nav_graph_1_start_fragment">
        <!-- nav graph stuff -->
    </navigation>
    
    <navigation
            android:id="@+id/main_nav_graph_2"
            android:label="@string/nav_graph_2_label"
            app:startDestination="@id/nav_graph_2_start_fragment">
        
        <fragment
                android:id="@+id/nav_graph_2_start_fragment"
                android:label="@string/nav_graph_2_start_fragment_label"
                android:name="my.package.ui.NavGraph2StartFragment"
                tools:layout="@layout/fragment_nav_graph_2_start">
        </fragment>
        <!-- In here is where I get the problem -->
    </navigation>
    
</navigation>
3 Answers

My Problem was the following:

After a Configuration change the NavGraph returned to its start destination, but the Fragment that was last active gets loaded anyway. This meant that the Fragment that was actually started and the current destination of the navGraph were out of sync.
When my Fragment then tried to load the navGraph scoped ViewModel, it failed because the navGraph thought it was on a different Fragment then it actually was.

To fix my problem I had to save and restore the state of the navController using the activities savedInstanceState Bundle. (source: https://stackoverflow.com/a/59987336)

override fun onSaveInstanceState(savedInstanceState: Bundle) {
    super.onSaveInstanceState(savedInstanceState)
    savedInstanceState.putBundle("nav_state", fragment.findNavController().saveState())
}

// restore in RestoreInstanceState
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
    super.onRestoreInstanceState(savedInstanceState)
    fragment.findNavController().restoreState(savedInstanceState.getBundle("nav_state"))
}

// or restore in onCreate
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    if (savedInstanceState?.containsKey("nav_state") == true) {
        fragment.findNavController().restoreState(savedInstanceState.getBundle("nav_state"))
    }
}

For anyone else coming here, I experienced this issue when a dialog was currently displayed and I issued a navController.navigate() call to a Fragment which uses by navGraphViewModels() before the dialog was dismissed.

It's as if the Navigation library couldn't understand that there is a destination with that ID available when a dialog is being displayed.

And for more clarity, this dialog is displayed using the <dialog> attribute in my nav graph XML.

My fix (for now) is to ensure the dialog is dismissed before making a further call to navigate elsewhere by way of view.postDelayed({ navController.navigate(elsewhere)}, 300)

This isn't the first time I've experienced issues with the NavController trying to navigate when a dialog is displayed.

Solved Easy by change this :

private val myViewModel: MyViewModel by hiltNavGraphViewModels(R.id.nav_graph_id)

To:

private val viewModel: AuthViewModel by activityViewModels{ defaultViewModelProviderFactory }
Related