Reputation: 386
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
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 :
Github gist link
Upvotes: 0
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
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
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
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