Search code examples
androidandroid-fragmentsandroidxandroid-viewmodel

AbstractSavedStateViewModelFactory: SavedStateProvider with the given key is already registered


Although it is the same exception, my situation is different from SavedStateProvider with the given key is already registered as I am using Nav-graph Scoped ViewModels,

Exception occurs when using AbstractSavedStateViewModelFactory with navGraphViewModels.
From startFragment, go to FirstPageFragment, navigateUp() back to startFragment, then visit FirstPageFragment again ->crash

class FirstPageFragment: Fragment() {

    private val myViewModel: MyViewModel by navGraphViewModels(R.id.nav_mission){
        MyViewModel.Factory(requireActivity(), "hello world1")
    }
    ...

My Factory

class MyViewModel(application: Application,
                  savedStateHandle: SavedStateHandle,
                  val someString: String) : AndroidViewModel(application){

    class Factory(val activity: Activity, val someString: String):
        AbstractSavedStateViewModelFactory(activity as SavedStateRegistryOwner, null) {

        override fun <T : ViewModel?> create(
            key: String,
            modelClass: Class<T>,
            handle: SavedStateHandle
        ): T {
            if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
                @Suppress("UNCHECKED_CAST")
                return MyViewModel(activity.application, handle, someString) as T
            }
            throw IllegalArgumentException("Unable to construct viewmodel")
        }
    }

...
}

This is my navGraph, ViewModel is for firstPageFragment and SecondPageFragment

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/nav_main_activity"
    app:startDestination="@id/startFragment">

    <fragment
        android:id="@+id/startFragment"
        android:name="com.example.savestatehandledemo.StartFragment"
        android:label="FirstPageFragment" >
        <action
            android:id="@+id/action_startFragment_to_nav_mission"
            app:destination="@id/nav_mission" />
    </fragment>

    <navigation android:id="@+id/nav_mission"
        app:startDestination="@id/firstPageFragment">
        <fragment
            android:id="@+id/firstPageFragment"
            android:name="com.example.savestatehandledemo.FirstPageFragment"
            android:label="FirstPageFragment" >
        </fragment>
        <fragment
            android:id="@+id/secondPageFragment"
            android:name="com.example.savestatehandledemo.SecondPageFragment"
            android:label="SecondPageFragment" >
        </fragment>
    </navigation>
</navigation>

I created a minimal example to reproduce the problem. https://github.com/yatw/saveStateHandleDemo/tree/master/app/src/main/java/com/example/savestatehandledemo
This exception occur only when going into a navigation graph.

Please help!


Solution

  • so I found the cause to this exception, I am passing in the activity as SavedStateRegistryOwner in AbstractSavedStateViewModelFactory.
    When visiting the navGraph the second time, I am passing in the same activity and the internal class SavedStateHandleController, SavedStateRegistry somehow saved the state already. (Whoever wrote this part please explain and write into the doc)

    So pass in the navGraph getBackStackEntry
    Updated viewModel factory

    class MyViewModel(application: Application,
                      savedStateHandle: SavedStateHandle,
                      val someString: String) : AndroidViewModel(application){
    
        class Factory(val application: Application,
                      val savedStateRegistryOwner: SavedStateRegistryOwner,
                      val someString: String):
            AbstractSavedStateViewModelFactory(
                savedStateRegistryOwner,
                null) {
    
            override fun <T : ViewModel?> create(
                key: String,
                modelClass: Class<T>,
                handle: SavedStateHandle
            ): T {
                if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
                    @Suppress("UNCHECKED_CAST")
                    return MyViewModel(application, handle, someString) as T
                }
                throw IllegalArgumentException("Unable to construct viewmodel")
            }
        }
    

    Use it in fragment

    class FirstPageFragment: Fragment() {
    
        private val myViewModel: MyViewModel by navGraphViewModels(R.id.nav_mission){
            MyViewModel.Factory(requireActivity().application,
                findNavController().getBackStackEntry(R.id.nav_mission),
                "hello world1")
        }
    

    Special thanks to EpicPandaForce, https://stackoverflow.com/a/61649394/5777189