Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Possible to access AndroidViewModel of Activity via Fragment?

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) 
    ```
like image 937
julianctni Avatar asked Feb 28 '20 12:02

julianctni


1 Answers

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).

like image 86
EpicPandaForce Avatar answered Oct 21 '22 09:10

EpicPandaForce