Possible to access AndroidViewModel of Activity via Fragment?
Asked Answered
K

2

9

In the summer of last year I started refactoring my Android application with Android's architecture components (Room, ViewModel, LiveData).

I have two Room repositories, one of them is accessed by multiple views (fragments) of the application. Because of that I used an AndroidViewModel, which has access to this repository and which is initialized in my MainActivity.

new ViewModelProvider(this).get(CanteensViewModel.class);

In my two fragments I accessed this ViewModel by

new ViewModelProvider(getActivity()).get(CanteensViewModel.class);

Until yesterday that worked perfectly. But then I updated my dependencies and since androidx.lifecycle version 2.2.0 this does not work anymore. I always get an exception (siehe EDIT 2):

Caused by: java.lang.InstantiationException: java.lang.Class<com.(...).CanteensViewModel> has no zero argument constructor

So I checked the docs and as I understood right I should/could now use

ViewModelProvider.AndroidViewModelFactory.getInstance(this.getApplication()).create(CanteensViewModel.class);

to get my ViewModel. But with this approach I can't add the owner (parameter of ViewModelProviders constructor), which results in the problem, that I can't really access the ViewModel I created in the Activity from inside my fragments.

Is there a way I can access the Activity's ViewModel from inside the fragments? Or would it be better to recreate the ViewModel in each fragment by

ViewModelProvider.AndroidViewModelFactory.getInstance(getActivity().getApplication()).create(CanteensViewModel.class);

instead of creating it inside the Activity?

EDIT: It seems to work, when I use the other constructor of ViewModelProvider, where a AndroidViewModelFactory is the second parameter.

new ViewModelProvider(this, ViewModelProvider.AndroidViewModelFactory.getInstance(this.getApplication())).get(CanteensViewModel.class);

Doing this in my MainActivity I can access the CanteensViewModel in my Fragment via

new ViewModelProvider(requireActivity()).get(CanteensViewModel.class);

EDIT 2 Stacktrace for above mentioned exception:

2020-02-28 14:30:16.098 25279-25279/com.pasta.mensadd E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.pasta.mensadd, PID: 25279
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.pasta.mensadd/com.pasta.mensadd.ui.MainActivity}: java.lang.RuntimeException: Cannot create an instance of class com.pasta.mensadd.ui.viewmodel.CanteensViewModel
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2795)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2873)
        at android.app.ActivityThread.-wrap11(Unknown Source:0)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1602)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:164)
        at android.app.ActivityThread.main(ActivityThread.java:6543)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
     Caused by: java.lang.RuntimeException: Cannot create an instance of class com.pasta.mensadd.ui.viewmodel.CanteensViewModel
        at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:221)
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:187)
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150)
        at com.pasta.mensadd.ui.MainActivity.onCreate(MainActivity.java:70)
        at android.app.Activity.performCreate(Activity.java:7023)
        at android.app.Activity.performCreate(Activity.java:7014)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1215)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2748)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2873) 
        at android.app.ActivityThread.-wrap11(Unknown Source:0) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1602) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:164) 
        at android.app.ActivityThread.main(ActivityThread.java:6543) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) 
     Caused by: java.lang.InstantiationException: java.lang.Class<com.pasta.mensadd.ui.viewmodel.CanteensViewModel> has no zero argument constructor
        at java.lang.Class.newInstance(Native Method)
        at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:219)
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:187) 
        at androidx.lifecycle.ViewModelProvider.get(ViewModelProvider.java:150) 
        at com.pasta.mensadd.ui.MainActivity.onCreate(MainActivity.java:70) 
        at android.app.Activity.performCreate(Activity.java:7023) 
        at android.app.Activity.performCreate(Activity.java:7014) 
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1215) 
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2748) 
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2873) 
        at android.app.ActivityThread.-wrap11(Unknown Source:0) 
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1602) 
        at android.os.Handler.dispatchMessage(Handler.java:106) 
        at android.os.Looper.loop(Looper.java:164) 
        at android.app.ActivityThread.main(ActivityThread.java:6543) 
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:438) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807) 
    ```
Krall answered 28/2, 2020 at 12:10 Comment(0)
J
18

So I checked the docs and as I understood right I should now use

ViewModelProvider.AndroidViewModelFactory.getInstance(
     this.getApplication()).create(CanteensViewModel.class);

Please share a link to this "docs" you mentioned, because this is NOT the first time I see this code, and yet it was equally wrong in both cases.

The code you actually should be using is

new ViewModelProvider(this).get(CanteensViewModel.class);

Is there a way I can access the Activity's ViewModel from inside the fragments? Or would it be better to recreate the ViewModel in each fragment by

new ViewModelProvider(requireActivity()).get(CanteensViewModel.class);

Consider also receiving a SavedStateHandle as an argument in your AndroidViewModel, and not only Application.


If you ask me, apparently the removal of ViewModelProviders.of() was an API mistake, but this is what we have now.




EDIT: With the help of the provided stack trace, I can finally somewhat figure out what's going on.

    at androidx.lifecycle.ViewModelProvider$NewInstanceFactory.create(ViewModelProvider.java:219)

We are using NewInstanceFactory as the default. What does default NewInstanceFactory do? It just calls no-arg constructor if available.

Wait, what? Isn't it supposed to fill in the Application for an AndroidViewModel?

Theoretically yes, as long as you got the original default ViewModelProvider.Factory, but this is not the one!

Why is it not the one that can fill in AndroidViewModel?

See this commit

Add default ViewModel Factory interface

Use a marker interface to allow instances of
ViewModelStoreOwner, such as ComponentActivity
and Fragment, to provide a default
ViewModelProvider.Factory that can be used with
a new, concise ViewModelProvider constructor.

This updates ComponentActivity and Fragment to
use that new API to provide an
AndroidViewModelFactory by default. It updates
the 'by viewModels' Kotlin extensions to use
this default Factory if one isn't explicitly
provided.

Also

ComponentActivity:

+    @NonNull
+    @Override
+    public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
+        if (getApplication() == null) {
+            throw new IllegalStateException("Your activity is not yet attached to the "
+                    + "Application instance. You can't request ViewModel before onCreate call.");
+        }
+        return ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication());
+    }
+

And most importantly

public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
    this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
            ? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
            : NewInstanceFactory.getInstance());
}

This means that you get the default view model provider factory that can properly set up AndroidViewModel if the ViewModelStoreOwner implements HasDefaultViewModelProviderFactory.

Theoretically, ComponentActivity is indeed a HasDefaultViewModelProviderFactory; and AppCompatActivity extends from ComponentActivity.

In your case however, that doesn't seem to be the case. For some reason, your AppCompatActivity is not HasDefaultViewModelProviderFactory.

I think the solution to your problem is to update Lifecycle to 2.2.0, and ALSO update implementation 'androidx.core:core-ktx to at least 1.2.0. (specifically at least AndroidX-Activity 1.1.0, and AndroidX-Fragment 1.2.0).

Jesselyn answered 28/2, 2020 at 12:17 Comment(7)
Thanks four your answer! I will add the link to the documentation in a second. As I already said, I already tried your proposal new ViewModelProvider(this).get(CanteensViewModel.class);, but then I get the java.lang.InstantiationException I also posted.Krall
That is very interesting, if you don't provide a factory to the ViewModelProvider, it should automatically choose the default factory, but you can try passing in a SavedStateViewModelFactory yourself. AFAIK the new default factory os SavedStateViewModelFactory, and it should instantiate AndroidViewModel correctly. I would need a COMPLETE stack trace to know where the ViewModel instantiation fails with the default factory you used. Maybe the issue is that AndroidViewModels require (Application, SavedStateHandle) as their default constructor arguments now.Jesselyn
I added the complete stack trace. I tried adding an AndroidViewModelFactory as second parameter, then it works (see EDIT 1).Krall
You have a very interesting problem, I'll edit my answer soon.Jesselyn
Thank you very much for your research and your answer! I will try that on monday, when I am back at work. Until now I did not even have the androidx.core, androidx.activity and androidx.fragment dependencies in my gradle file. Only androidx.appcompat, from which I am getting the AppCompatActivity. My fragments extend from androidx.fragment.app.Fragment. I think I have to update/enhance my androidx knowledge :oKrall
It worked! I added androidx.fragment:fragment:1.2.2 and androidx.core:core:1.2.0 to my dependencies. ;)Krall
If the answer helped, you should accept it with the tick mark ;) happy to hear it worked outJesselyn
M
0

Stumbled upon this thread while searching for a similar problem, but in my case I was simply trying to get an instance of AndroidViewModel from my activity. I was presented with the same same zero constructor error. Adding implementation "androidx.fragment:fragment-ktx:1.2.5" solved the issue for me even though I'm not using any fragments in my app.

Modernity answered 25/11, 2020 at 19:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.