androidandroid-viewmodel

Create a shared ViewModel with Factory


I have a DocumentsFragment with a TabLayout with 3 tabs:

TabRulesFragment, TabProceduresFragment, TabGuidanceFragment

In DocumentsFragment I initialize a shared viewModel, DocumentsSharedViewModel with a factory:

class DocumentsFragment : Fragment() {

    private lateinit var sharedViewModel: DocumentsSharedViewModel
    private lateinit var viewPager2: ViewPager2
    private lateinit var documentsCollectionAdapter: DocumentsCollectionAdapter

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {

        val program = DocumentsFragmentArgs.fromBundle(requireArguments()).program
        val name = DocumentsFragmentArgs.fromBundle(requireArguments()).name

        val viewModelFactory = DocumentsSharedViewModelFactory(program, name)
        sharedViewModel = ViewModelProvider(this, viewModelFactory)[DocumentsSharedViewModel::class.java]

to share data between the documents fragment and the 3 tab fragments. When I try to connect to the shared viewModel in one of the tab fragments (TabRulesFragment for example):

class TabRulesFragment : Fragment() {

    private lateinit var tabRulesRecyclerView: RecyclerView

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {

        val sharedViewModel : DocumentsSharedViewModel by viewModels()
        val binding = TabRulesFragmentBinding.inflate(layoutInflater)
        binding.viewModel = sharedViewModel

I get an error that I can't create an instance of the DocumentsSharedViewModel:

java.lang.RuntimeException: Cannot create an instance of class com.smellydogcoding.westvirginiaelectronicfieldguide.ui.documents.DocumentsSharedViewModel
        at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.kt:188)
        at androidx.lifecycle.ViewModelProvider$AndroidViewModelFactory.create(ViewModelProvider.kt:238)
        at androidx.lifecycle.SavedStateViewModelFactory.create(SavedStateViewModelFactory.java:112)
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:169)
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.kt:139)
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:44)
        at androidx.lifecycle.ViewModelLazy.getValue(ViewModelLazy.kt:31)
        at com.smellydogcoding.westvirginiaelectronicfieldguide.ui.documents.rulesTab.TabRulesFragment.onCreateView$lambda-0(TabRulesFragment.kt:30)
        at com.smellydogcoding.westvirginiaelectronicfieldguide.ui.documents.rulesTab.TabRulesFragment.onCreateView(TabRulesFragment.kt:32)

I'm assuming that viewModelProvider is looking for the factory (which doen't exist in TabRulesFragment because it's in DocumentsFragment) and throwing an error when it doesn't find it. Is there any way to use data from a shared data model without creating another instance of it?


Solution

  • If you want to make a ViewModel scoped to the owning Activity that you can share between fragments, you can use the following to get it in both fragments.

    val sharedModel: DocumentsSharedViewModel by activityViewModels()
    

    according to the docs, which has this simple example, where both fragments can access the same ViewModel

    class ListFragment : Fragment() {
    
        // Use the 'by activityViewModels()' Kotlin property delegate
        // from the fragment-ktx artifact
        private val model: SharedViewModel by activityViewModels()
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            //...
        }
    }
    
    class DetailFragment : Fragment() {
    
        // Use the 'by activityViewModels()' Kotlin property delegate
        // from the fragment-ktx artifact
        private val model: SharedViewModel by activityViewModels()
    
        override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
            super.onViewCreated(view, savedInstanceState)
            //...
        }
    }
    

    As pointed out in the comments, if you wanted the ViewModel to remain scoped to the parent fragment instead of using the activity scope you could use this instead to access it in the child fragment

    val sharedModel: DocumentsSharedViewModel by viewModels({ requireParentFragment() })