I have a fragment HomeFragment
which is part of the app navigation graph. Within FragmentHome
I have a viewpager with 3 fragment instances of same class ChildFragment
. Each ChildFragment
requires an instance of ChildViewModel
defined as follows
@HiltViewModel
class ChildViewModel @Inject constructor(
private val repo: Repository
) : ViewModel() {
}
How can I create different instances of my viewmodel ChildViewModel
that is scoped to the lifecycle of HomeFragment
nav destination?
I tried the following line. But this shares the same viewmodel instance across all 3 ChildFragments
which is not desired.
private val vm by hiltNavGraphViewModels(R.id.homeFragment)
From what I understand, the ViewModelStore uses a Key which is the Canonical name of the viewmodel for saving it. Is there a way I can use a custom key for each fragment so that each ChildFragment
get its own instance?
I also tried
private lateinit var vm: ChildViewModel
...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
parentFragment?.let {
vm = ViewModelProvider(it).get(args.sectionId, CihldViewModel::class.java)
}
}
But this threw some exception regarding it unable to create instance of my ViewModel. I'm a bit unfamiliar with using Viewmodel Factory approach since Hitl takes care of it for us.
The reason I want to scope this viewmodel to the HomeFragment
nav destination is for the savedState, so that I can reuse the same respective viewmodel instance for each child fragment when the user lands back on HomeFragment
So, after a lot of digging and reading, I finally figured it out, well sort of. Initially I was hoping to achieve this using CreationExtras or Hilt AssistedInject. But unfortunately, those didn't work.
The reason being, in order for the custom key to work, we have to pass that custom key to ViewModelProvider.get()
and none of the below delegates pass a string key to it. Hence, a ViewModel instance is created and saved with the default key.
by viewModels()
by activityViewModels()
by navGraphViewModels()
by hiltNavGraphViewModels()
Surprisingly, this is possible in Compose via hiltViewModel(viewModelStoreOwner: ViewModelStoreOwner, key: String?)
but the same isn't available for Fragments.
Anyway, here's the solution that worked for me. If you dig into by hiltNavGraphViewModels()
you'll notice that it simply creates a ViewModelProvider with default param values, with the exception of the HiltViewModelFactory
which is responsible for injecting the dependencies into our HiltViewModel
annotated ViewModel. So I simply create a ViewModelProvider with the same defaults and pass my custom key as required.
private lateinit var vm: ChildViewModel
...
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
parentFragment?.let { it ->
val backStackEntry = it.findNavController().getBackStackEntry(R.id.homeFragment)
vm = ViewModelProvider(
it,
HiltViewModelFactory(
requireActivity(),
backStackEntry.defaultViewModelProviderFactory
)
)[args.sectionId, ChildViewModel::class.java]
}
}
EDIT: You can also create your own lazy delegate extension function as follows:
inline fun <reified VM : ViewModel> Fragment.hiltNavGraphViewModels(
key: String,
@IdRes navGraphId: Int
): Lazy<VM> = lazy {
val backStackEntry = findNavController().getBackStackEntry(navGraphId)
ViewModelProvider(
owner = backStackEntry,
factory = HiltViewModelFactory(requireActivity(), backStackEntry)
)[key, VM::class.java]
}
References: