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.
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~~ #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.
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
I want to have more than one same Composable as a childs of a parent Composable. Each one of them have to request different data to show it to the user.
Is there any way to create different instance of ViewModel to each Composable? By default the ViewModel it's associated to the single activity of the project I think, but I don't know if I could have different ViewModel instances or if I've to share ViewModel and manage the state of each composable inside the ViewModel.
I cannot create different ViewModel because I don't know how many Composables of this type we'll have (I receive all of the dynamically).
Currently I've using Hilt
#HiltViewModel
class SampleViewModel #Inject constructor(
private val sampleUseCase: sampleUseCase,
): ViewModel() {
....
}
I've been looking for a solution but I haven't found none.
When I want to share a view model between various views I would use by viewmodels. I recently began looking into hilt and was wondering if hiltviewmodel would accomplish the same thing? That is to allow me to share a single(same instance) of a viewmodel?
by viewModels():
property delegate.
the first time create, next time return the same instance(in the same scope, the scope is the same activity)
= hiltViewModel():
only used in #Composable annotated function.
the first time create, next time return the same instance(in the same scope, the scope is NavGraph, if no graph, fragment/activity)
you 'd best build a demo and practise it, and log the instance to see if the same instance.
val viewModel: ViewModelA by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
HiltDemoTheme {
val viewModel2: ViewModelA = hiltViewModel()
val viewModel3: ViewModelA = viewModel()
val viewModel4: ViewModelA by viewModels()
Log.d("Jeck", "$viewModel")
Log.d("Jeck", "$viewModel2")
Log.d("Jeck", "$viewModel3")
Log.d("Jeck", "$viewModel4")// the four get the same instance
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colors.background
) {
}
}
}
Documentation:
Hilt and Navigation
When using Navigation Compose, always use the hiltViewModel composable
function to obtain an instance of your #HiltViewModel annotated
ViewModel. This works with fragments or activities that are annotated
with #AndroidEntryPoint.
The ViewModel annotated with #HiltViewModel will be available for creation by the HiltViewModelFactory and can be retrieved by default in an Activity or Fragment annotated with #AndroidEntryPoint by using ViewModelProvider or the by viewModels().
Also, by viewModels returns a property delegate to access ViewModel by default scoped to the fragment. Remember viewModels is not only used with Hilt, but it can be used to provide simple ViewModel instances as well when not using Hilt.
On the other hand, #HiltViewModel, tells Hilt that this ViewModel can be injected into other classes marked with #AndroidEntryPoint as well as allowing Hilt to inject other dependencies into this ViewModel.
All Hilt ViewModels are provided by the ViewModelComponent which follows the same lifecycle as a ViewModel, and as such, can survive configuration changes. To scope a dependency to a ViewModel use the #ViewModelScoped annotation.
A #ViewModelScoped type will make it so that a single instance of the scoped type is provided across all dependencies injected into the ViewModel. Other instances of a ViewModel that request the scoped instance will receive a different instance.
If a single instance needs to be shared across various ViewModels, then it should be scoped using either #ActivityRetainedScoped or #Singleton.
[Source: https://developer.android.com/training/dependency-injection/hilt-jetpack#viewmodelscoped]
And if the shared/single ViewModel is among different fragments, you could bind the lifecycle of the injected ViewModel to the parent Activity by by activityViewModels.
More information:
https://dagger.dev/api/latest/dagger/hilt/android/lifecycle/HiltViewModel.html &
https://dagger.dev/hilt/view-model.html &
https://developer.android.com/training/dependency-injection/hilt-jetpack
in my app I have a MainActivity which requires access to a ViewModel. I am injecting the ViewModel using DaggerHilt and the #ViewModelInject annotation. Additionally, I have two Fragments within the Activity that require access to the same ViewModel in order to pass data to each other using observables.
The problem:
I have found that whenever one of my Fragments go through onDestroy() its ViewModel is killed. This leads me to think that the Activity and Fragments are not sharing the same ViewModel.
My question:
Does anyone know if we are supposed to use scope annotations for ViewModels in Dagger Hilt? I didn't see this stated in the Hilt docs or the android dev tutorials/guides. I had assumed that they were making ViewModels app level singletons, which would make sense.
If we do have to use scope annotations for ViewModels, does anyone know which level is appropriate?
This is my viewmodel code:
class MainActivityViewModel #ViewModelInject constructor(
private val repo: Repo,
private val rxSharedPrefsRepo: RxSharedPrefsRepo,
private val resourcesRepo: ResourcesRepo,
#Assisted private val savedStateHandle: SavedStateHandle
) : ViewModel() {
As per the Scoping in Android and Hilt blog post, using #ViewModelInject means that the objects you pass into the ViewModel are scoped to the ViewModel.
The scope of the ViewModel, however, is based on how you get the ViewModel (what ViewModelStore the ViewModel is associated with) - not anything that Hilt controls. If you use by viewModels() in a Fragment, then the ViewModel is scoped to the Fragment. If you use by activityViewModels() or by navGraphViewModels(), then the ViewModel would be scoped to the activity or navigation graph, respectively.
As mentioned in the blog post, if you want an object that is scoped to the activity and survives configuration changes, you can use Hilt's #ActivityRetainedScoped on any object and inject that object into both fragments.
Whether you should use #ActivityRetainedScoped or a ViewModel where you control the scope separately from Hilt is covered in the blog post:
The advantage of scoping with Hilt is that scoped types are available in the Hilt component hierarchy whereas with ViewModel, you have to manually access the scoped types from the ViewModel.
The advantage of scoping with ViewModel is that you can have ViewModels for any LifecycleOwner objects in your application. For example, if you use the Jetpack Navigation library, you can have a ViewModel attached to your NavGraph.
Hilt provides a limited number of scopes. You might find that you don’t have a scope for your particular use case — for example, when using nested fragments. For that case, you can fall back to scoping using ViewModel.
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.
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~~ #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.
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.
Why should I use viewmodelproviders for viewmodels?
Why I just can't add custom singleton annotation to my viewmodel, and then inject this viewmodel to fragment class?
Like so:
#MainScope
class MainViewModel #Inject constructor(): ViewModel()
And then:
open class BaseFragment<T: ViewModel>: DaggerFragment() {
#Inject
protected lateinit var viewModel: T
Both cases are independent of screen rotation.
Is there any drawbacks of singleton annotation case?
I see only advantages, with this approach I don't need to copy/paste tons of code.
Why should I use viewmodelproviders for viewmodels?
To get viewModel.onCleared() callback called properly at the right time by the ComponentActivity.
(and to ensure it's created only once for the given ViewModelStoreOwner).
Why I just can't add custom singleton annotation to my viewmodel, and then inject this viewmodel to fragment class?
Because you won't get viewModel.onCleared() callback called properly at the right time by the ComponentActivity.
Is there any drawbacks of singleton annotation case? I see only advantages,
That you don't get viewModel.onCleared().
Also if you have a singleton variant, then the ViewModel won't die along with its enclosing finishing Activity, and stay alive even on back navigation (which is probably not intended).
with this approach I don't need to copy/paste tons of code.
You're using Kotlin. Use extension functions.