ritesh4302
ritesh4302

Reputation: 386

Hilt creating different instances of view model inside same activity

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.

Upvotes: 15

Views: 16608

Answers (5)

Ahmed Atwa
Ahmed Atwa

Reputation: 53

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

Upvotes: 0

EpicPandaForce
EpicPandaForce

Reputation: 81549

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.

Upvotes: 9

Kaan
Kaan

Reputation: 83

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.

Upvotes: 0

AaronC
AaronC

Reputation: 314

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.

Upvotes: 3

ianhanniballake
ianhanniballake

Reputation: 199880

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>()

Upvotes: 39

Related Questions