Search code examples
androidkotlinmvvmviewmodelandroid-viewmodel

Caused by: java.lang.RuntimeException: Cannot create an instance of class com.app.MyViewModel


I got into to an issue with accessing viewmodel.

I have an activity and 2 fragments in it. I have a view model for the activity and fragment in using the same instance of the view model created in host activity.

class MyViewModel(var paymentDataModel: PaymentDataModel) : ViewModel(){

   fun someMethod():Boolean{
   //return Something 
}
}

class MyViewModelFactory(var paymentDataModel: PaymentDataModel) : ViewModelProvider.Factory {

    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        return MyViewModel(paymentDataModel) as T
    }
}

class NewPaymentAmountFragment : Fragment() {
    private val paymentViewModel: MyViewModel by activityViewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        if(paymentViewModel.someMehtod()){ 
   //Accessing activity viewmodel in fragment
     }
    }
}

If I define viewmodel using viewModel extension in activity function it says the below error.

Caused by: java.lang.RuntimeException: Cannot create an instance of class com.app.MyViewModel

    class MyActivity : BaseActivity(){
    
    val myViewModel: MyViewModel by viewModels { 
    MyViewModelFactory(constructPaymentDataModel()) }

override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
        }
    }

But if I define ViewModel in a normal way using ViewModelProvider() its working.

class MyActivity : BaseActivity(){

lateint var myViewModel: MyViewModel 

   override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val viewModelFactory = MyViewModelFactory(constructPaymentDataModel())
        myViewModel = ViewModelProvider(this, viewModelFactory)[MyViewModel::class.java]
    }
}

Also this happens only when viewmodel in fragment is accessed first.

If I accessed viewmodel in activity once before oncreate of activity , in fragment its working fine. Its able to get the viewmodel instance.

class MyActivity : BaseActivity(){

val myViewModel: MyViewModel by viewModels { 
MyViewModelFactory(constructPaymentDataModel()) }

 override fun onCreate(savedInstanceState: Bundle?) {
        println(myViewModel.isPaymentMethodExists.value)
        super.onCreate(savedInstanceState)
}
}

Here I accessed viewmodel before fragment accessing activities viewmodel. So here viewmodel is assigned by lazy when breakpoint comes to this println method.

The same , if I access viewmodel in fragment first . The lazy viewmodel in activity does not get assigned.

So here is the summary, if viewmodel is defined in both activity and fragment using viewmodel extensions and viewmodel is accessed in fragment first, its not working.


Solution

  • You're not passing the factory when you access the ViewModel in the Fragment:

    private val paymentViewModel: MyViewModel by activityViewModels()
    

    You need to do the same as in the Activity - they both need to be able to construct the VM if necessary, so it's the same code:

    // still using the activityViewModels delegate, because you want the Activity's VM instance
    private val paymentViewModel: MyViewModel by activityViewModels {
        MyViewModelFactory(constructPaymentDataModel())
    }
    

    The way ViewModels work is each Activity and Fragment class (also backstack entries if you're using the Navigation library) can each have their own single instance of a VM. This allows different instances of the same Activity or whatever to grab the same VM object after being destroyed - the VM outlives them, and they share it.

    It also allows other components to grab the specific instance belonging to an Activity (or whatever). That's how Fragments can grab their parent Activity's copy of a particular VM and share data with each other, because they're all looking at the same instance.


    To get a VM, you call ViewModelProvider(owner) (where owner is the Activity or Fragment that owns the instance) and then call get(SomeViewModel::class.java) to say which type of VM you want to grab. If there's already an instance of that VM associated with this owner, it'll return it - that's how everything gets to share the same VM object.

    (by viewModels and by activityViewModels are just nice shorthand for this - they just call ViewModelProvider(this) or viewModelProvider(parentActivity) respectively, to pass the relevant owner and get the required VM)

    If there isn't an instance of this VM, then the provider will create one and store it for anything else that requests it. This is the key part - the first thing that requests a VM instance for a particular owner is what causes it to be created. If that creation requires a factory (like in your case), the first request needs to provide that factory.

    So when your Activity is the first to request the VM, it provides the factory which is used to create it. Then when the Fragment makes the request later, it doesn't matter that it's not providing a factory function - the VM instance is already stored. But do it the other way around, and you run into a problem. Because the Fragment makes the request, and doesn't provide a factory, it tries to use the default no-args factory. And that won't work for your VM class with its PaymentDataModel parameter, so you get the error. Provide the factory everywhere you request the VM, and it'll work.

    Hope that clears it up!