Hilt creating different instances of view model inside same activity
Asked Answered
U

5

14

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.

Upsydaisy answered 24/6, 2020 at 16:42 Comment(3)
Do not annotate your ViewModel with @Singleton.Luann
Why you are annotating your view model with @Singleton ?Garfieldgarfinkel
Yes, i have removed it.Upsydaisy
S
39

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>()
Shrive answered 24/6, 2020 at 17:16 Comment(1)
it works , val actViewModel: <ActivityViewModel> by activityViewModels()Leatrice
L
9

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~~ @HiltViewModel and @Inject constructor 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 use val vm = androidx.hilt.navigation.fragment.hiltNavGraphViewModels(R.navigation.nav_graph_id).

If you are not using Hilt, then you can use = navGraphViewModels but you can get the SavedStateHandle using either the default ViewModelProvider.Factory, or the CreationExtras.

Luann answered 24/6, 2020 at 17:39 Comment(3)
Note that an @ActivityRetainedScope object is stored within a activity scoped ViewModel, so the lifetime of that object vs the lifetime of an activity scoped ViewModel itself will be exactly the same.Shrive
This is true, which is exactly why I said it should generally be stateless, or the state in it should be expected to be transient (as you can't see the SavedStateHandle that would belong to the enclosing SavedStateHandle). The idea there was that if you'd use an Activity-scoped VM and you don't need a SavedStateHandle, then you can do that with Hilt without requiring the VM as a holder (or extending VM).Luann
it is not deprecated in androidx.hilt:hilt-navigation-fragment:1.0.0, in some cases this is definitely a better solution than activity scoped view modelFrentz
L
3

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.

Longwinded answered 10/10, 2021 at 2:54 Comment(1)
I like this approach.Brookweed
G
0

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.

Glandulous answered 15/9, 2022 at 1:35 Comment(0)
P
0

You can add the following extension to your code and you will be able to retrieve the same instance of any ViewModel of a previous fragment in the back stack.

@MainThread
public inline fun <reified VM : ViewModel> Fragment.backStackViewModels(
    @IdRes destinationId: Int
): Lazy<VM?> = lazy {
    findNavController().currentBackStack.value.lastOrNull { it.destination.id == destinationId }
        ?.let { backStackEntry ->
            ViewModelLazy(
                VM::class,
                { backStackEntry.viewModelStore },
                { HiltViewModelFactory(requireActivity(), backStackEntry) },
                { defaultViewModelCreationExtras }
            ).value
        } ?: run { null }

The viewModel must be created in the owner fragment for the first time using hiltNavGraphViewModels as following :

private val mViewModel by hiltNavGraphViewModels<SomeViewModel>(R.id.someDestination)

The same instance of the viewModel can then be retrieved in any other later fragment using backStackViewModels as following:

private val mViewModel by backStackViewModels<SomeViewModel>(R.id.someDestination)

Note :

  • Destination can either be a nav graph's id or a destination's id for any fragment in any nav graph.
  • Destination must be same in both fragments to retrieve the same instance of the cached viewModel.
  • In case the destination doesn't exist in the later fragment's back stack then null would be returned.

Github gist link

Phony answered 23/9, 2023 at 11:38 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.