Возможно ли получить доступ к AndroidViewModel активности через фрагмент? - PullRequest
3 голосов
/ 28 февраля 2020

Летом прошлого года я начал рефакторинг своего приложения Android с компонентами архитектуры Android (Room, ViewModel, LiveData).

У меня есть два репозитория Room, к одному из которых обращаются несколькими взглядами (фрагментами) приложения. Из-за этого я использовал AndroidViewModel, который имеет доступ к этому хранилищу и который инициализирован в моем MainActivity.

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

В двух моих фрагментах я получил доступ к этой модели представления с помощью

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

До вчерашнего дня это работало отлично. Но затем я обновил свои зависимости и, начиная с androidx.lifecycle версии 2.2.0, это больше не работает. Я всегда получаю исключение (см. РЕДАКТИРОВАТЬ 2):

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

Поэтому я проверил документы и, как я понял правильно, я должен / мог теперь использовать

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

чтобы получить мою ViewModel. Но при таком подходе я не могу добавить owner (параметр конструктора ViewModelProvider s), что приводит к проблеме, из-за которой я не могу получить доступ к ViewModel, который я создал в Activity, изнутри своих фрагментов.

Есть ли способ получить доступ к ViewModel Activity из фрагментов? Или было бы лучше воссоздать ViewModel в каждом фрагменте с помощью

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

вместо создания его внутри Activity?

EDIT: Кажется, это работает, когда я использую другой конструктор из ViewModelProvider, где AndroidViewModelFactory является вторым параметром.

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

Делая это в моем MainActivity, я могу получить доступ к CanteensViewModel в моем Fragment через

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

РЕДАКТИРОВАТЬ 2 Stacktrace для вышеупомянутого исключения:

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

1 Ответ

3 голосов
/ 28 февраля 2020

Итак, я проверил документы и, как я правильно понял, теперь я должен использовать

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

Пожалуйста, поделитесь ссылкой на эти "документы", которые вы упомянули, потому что это НЕ первое когда я вижу этот код, и все же он был в равной степени неправильным в обоих случаях.

Код, который вы на самом деле должны использовать:

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

Есть ли способ получить доступ к ViewModel активности из фрагментов? Или было бы лучше воссоздать ViewModel в каждом фрагменте путем

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

Рассмотрим также получение SavedStateHandle в качестве аргумента в вашем AndroidViewModel, а не только Application .


Если вы спросите меня, очевидно, удаление ViewModelProviders.of() было ошибкой API, но это то, что мы имеем сейчас.




РЕДАКТИРОВАТЬ: С помощью предоставленной трассировки стека, я наконец-то могу немного понять, что происходит.

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

Мы используем NewInstanceFactory по умолчанию. Что делает по умолчанию NewInstanceFactory? Он просто вызывает конструктор без аргументов , если доступен.

Подождите, что? Разве это не должно заполнять Application для AndroidViewModel?

Теоретически да, если вы получили исходное значение по умолчанию ViewModelProvider.Factory, но это не то!

Почему это не тот, который может заполнить AndroidViewModel?

См. этот коммит

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.

Также

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());
+    }
+

И самое главное

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

Это означает, что вы получаете фабрику поставщика модели представления по умолчанию, которая может правильно настроить AndroidViewModel if ViewModelStoreOwner реализует HasDefaultViewModelProviderFactory.

Теоретически, ComponentActivity действительно HasDefaultViewModelProviderFactory; и AppCompatActivity простирается от ComponentActivity.

В вашем случае, однако, это не так. По какой-то причине ваш AppCompatActivity не является HasDefaultViewModelProviderFactory.

Я думаю, что решение вашей проблемы - обновить Lifecycle до 2.2.0, а ТАКЖЕ обновить implementation 'androidx.core:core-ktx до как минимум 1.2.0. (в частности, как минимум AndroidX-Activity 1.1.0 и AndroidX-Fragment 1.2.0).

...