'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 1:[1]

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

Solution 2:[2]

When you use kotlin compose with compose navigation and want to use the AbstractSavedStateViewModelFactory, you have to pass, like mentioned in the accepted solution above, the navBackStackEntry. In compose this is given in the specific composable() { navBackStackEntry -> }

Example of Factory:

class SavedStateViewModelFactory(
private val repository: LocationRepository,
defaultArgs: Bundle? = null,
savedStateRegistryOwner: SavedStateRegistryOwner) : AbstractSavedStateViewModelFactory(
savedStateRegistryOwner,
defaultArgs) {
override fun <T : ViewModel?> create(
    key: String,
    modelClass: Class<T>,
    handle: SavedStateHandle
): T {
    return when {
        modelClass.isAssignableFrom(LocationViewModel::class.java) -> {
            LocationViewModel(repository, handle) as T
        }
        modelClass.isAssignableFrom(LocationsViewModel::class.java) -> {
            LocationsViewModel(repository, handle) as T
        }
        else -> throw IllegalArgumentException("wrong ViewModel")
    }

}}

Example in setContent():

Surface(
                modifier = Modifier.fillMaxSize(),
                color = MaterialTheme.colors.background
            ) {
                val navController = rememberNavController()
                NavHost(
                    navController = navController,
                    startDestination = Screens.LocationsScreen.route,
                    ) {
                    composable(
                        route = Screens.LocationsScreen.route
                    ) { navBackStackEntry ->
                        LocationsScreen(
                            navController,
                            viewModel(
                                modelClass = LocationsViewModel::class.java,
                                factory = SavedStateViewModelFactory(
                                    repository = LocationRepositoryImpl(
                                        BarCounterDatabase.getINSTANCE(application).locationDao
                                    ),
                                    savedStateRegistryOwner = navBackStackEntry
                                )
                            )
                        )
                    }
                    composable(
                        route = Screens.LocationScreen.route+"{id}",
                        arguments = listOf(
                            navArgument(
                                name = "id"
                            ) {
                                type = NavType.LongType
                            }
                        )
                    ) { navBackStackEntry ->
                        var defaultArgs: Bundle? = null
                        navBackStackEntry ->.arguments?.getLong("id")?.let { id ->
                            defaultArgs = Bundle()
                            defaultArgs?.putLong("id", id)
                        }
                        LocationScreen(
                            navController = navController,
                            viewModel(
                                modelClass = LocationViewModel::class.java,
                                factory = SavedStateViewModelFactory(
                                    repository = LocationRepositoryImpl(
                                        BarCounterDatabase.getINSTANCE(application).locationDao
                                    ),
                                    savedStateRegistryOwner = navBackStackEntry,
                                    defaultArgs = defaultArgs
                                )
                            )
                        )
                    }
                }
            }

Oh, not to forget: I figured this out only with the answer of BabyishTank

Without that answer i would still be foolish

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 BabyishTank
Solution 2