Hilt creating different instances of view model inside same activity

Viewed 6577

After recently migrating from Dagger to Hilt I started observing very strange behavior with respect to ViewModels. Below is the code snippet:


@HiltAndroidApp
class AndroidApplication : Application() {}

@Singleton
class HomeViewModel @ViewModelInject constructor() :
    ViewModel() {}

@AndroidEntryPoint
class HomeFragment : Fragment(R.layout.fragment_home) {

    private val homeViewModel by viewModels<HomeViewModel>()

    override fun onResume() {
        super.onResume()
        Timber.i("hashCode: ${homeViewModel.hashCode()}")
    }
}


@AndroidEntryPoint
class SomeOtherFragment : Fragment(R.layout.fragment_home) {

    private val homeViewModel by viewModels<HomeViewModel>()

    override fun onResume() {
        super.onResume()
        Timber.i("hashCode: ${homeViewModel.hashCode()}")
    }
}

The value of hashCode isn't consistent in all the fragments. I am unable to figure out what else am I missing for it to generate singleton instance of viewmodel within the activity.

I am using single activity design and have added all the required dependencies.

4 Answers

When you use by viewModels, you are creating a ViewModel scoped to that individual Fragment - this means each Fragment will have its own individual instance of that ViewModel class. If you want a single ViewModel instance scoped to the entire Activity, you'd want to use by activityViewModels

private val homeViewModel by activityViewModels<HomeViewModel>()

What Ian says is correct, by viewModels is the Fragment's extension function, and it will use the Fragment as the ViewModelStoreOwner.

If you need it to be scoped to the Activity, you can use by activityViewModels.

However, you typically don't want Activity-scoped ViewModels. They are effectively global in a single-Activity application.

To create an Activity-global non-stateful component, you can use the @ActivityRetainedScope in Hilt. These will be available to your ViewModels created in Activity or Fragment.

To create stateful retained components, you should rely on @ViewModelInject, and @Assisted to get a SavedStateHandle.

There is a high likelihood that at that point, instead of an Activity-scoped ViewModel, you really wanted a NavGraph-scoped ViewModel.

To get a SavedStateHandle into a NavGraph-scoped ViewModel inside a Fragment via Hilt's @Assisted annotation, you can (EDIT: can't) use:

//@Deprecated
//inline fun <reified T : ViewModel> Fragment.hiltNavGraphViewModels(@IdRes navGraphIdRes: Int) =
//viewModels<T>(
//    ownerProducer = { findNavController().getBackStackEntry(navGraphIdRes) },
//    factoryProducer = { defaultViewModelProviderFactory }
//)

.

EDIT: Due to https://github.com/google/dagger/issues/2152 this above approach doesn't work, so what can work is to use accessors and building the NavGraph-scoped AbstractSavedStateViewModelFactory directly with accessors. It's a bit messy at the moment because ActivityRetainedComponent is hard to access, so stay tuned for a better solution...

Here's an alternative solution to what ianhanniballake mentioned. It allows you to share a view model between fragments while not assigning it to the activity, therefore you avoid creating essentially a global view model in a single activity as EpicPandaForce stated. If you're using Navigation component, you can create a nested navigation graph of the fragments that you want to share a view model (follow this guide: Nested navigation graphs)

Within each fragment:

private val homeViewModel: HomeViewModel
    by navGraphViewModels(R.id.nested_graph_id){defaultViewModelProviderFactory}

When you navigate out of the nested graph, the view model will be dropped. It will be recreated when you navigate back into the nested graph.

As mentioned by other posts here, using the by activityViewModels<yourClass>() will scope the VM to the entire Activity's lifecycle, making it effectively a global scope, to the entire app, if it's one activity architecture everyone uses and Google recommends.

Clean, minimal solution: If you're using nav graph scoped viewmodels:

Replace this:

val vm: SomeViewModel by hiltNavGraphViewModels(R.id.nav_vm_id)

with below:

val vm by activityViewModels<SomeViewModel>()

This allows me to use this VM as a sharedviewmodel between the activity and those fragments.

Otherwise even the livedata observers do not work, as it creates new instances and lifecycles that are independent from each other.

Related