androidandroid-fragmentsviewmodelandroid-viewmodeldagger-hilt

How to create different viewmodel instance for fragments in viewpager scoped to parent fragment with Hilt


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


Solution

  • 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: