Как получить контекст в Android MVVM ViewModel

90

Я пытаюсь реализовать шаблон MVVM в своем приложении для Android. Я читал, что ViewModels не должен содержать специального кода для Android (чтобы упростить тестирование), однако мне нужно использовать контекст для различных вещей (получение ресурсов из xml, инициализация настроек и т. Д.). Как лучше всего это сделать? Я видел, что в нем AndroidViewModelесть ссылка на контекст приложения, но он содержит код, специфичный для Android, поэтому я не уверен, что это должно быть в ViewModel. Также они связаны с событиями жизненного цикла Activity, но я использую кинжал для управления набором компонентов, поэтому я не уверен, как это повлияет на это. Я новичок в шаблоне MVVM и Dagger, поэтому приветствую любую помощь!

Винсент Уильямс
источник
На всякий случай, если кто-то пытается использовать, AndroidViewModelно получает его, Cannot create instance exceptionвы можете обратиться к моему этому ответу stackoverflow.com/a/62626408/1055241
gprathour
Вы не должны использовать Context в ViewModel, вместо этого создайте UseCase, чтобы получить контекст таким образом
Рубен Кастер,

Ответы:

71

Вы можете использовать Applicationконтекст, который предоставляется AndroidViewModel, вы должны расширить, AndroidViewModelкоторый просто ViewModelвключает Applicationссылку.

Джей
источник
Работал как шарм!
SPM
Может ли кто-нибудь показать это в коде? Я на Яве
Бисвас Хаярголи
55

Для модели представления компонентов архитектуры Android,

Не рекомендуется передавать свой контекст действия в ViewModel действия, поскольку это утечка памяти.

Следовательно, чтобы получить контекст в вашей ViewModel, класс ViewModel должен расширять класс Android View Model . Таким образом вы можете получить контекст, как показано в примере кода ниже.

class ActivityViewModel(application: Application) : AndroidViewModel(application) {

    private val context = getApplication<Application>().applicationContext

    //... ViewModel methods 

}
devDeejay
источник
2
Почему бы не использовать напрямую параметр приложения и обычную ViewModel? Не вижу смысла в "getApplication <Application> ()". Он просто добавляет шаблон.
Невероятное
50

Дело не в том, что модели ViewModels не должны содержать специфичный для Android код для упрощения тестирования, поскольку это абстракция, которая упрощает тестирование.

Причина, по которой модели представления не должны содержать экземпляр контекста или что-то вроде представлений или других объектов, которые хранятся в контексте, заключается в том, что у него отдельный жизненный цикл, чем у действий и фрагментов.

Я имею в виду, что вы меняете ротацию в своем приложении. Это приводит к тому, что ваша активность и фрагмент разрушаются, поэтому они воссоздают себя. ViewModel предназначен для сохранения в этом состоянии, поэтому есть вероятность сбоев и других исключений, если он все еще удерживает View или Context для уничтоженной Activity.

Что касается того, как вы должны делать то, что хотите делать, MVVM и ViewModel действительно хорошо работают с компонентом привязки данных JetPack. Для большинства вещей, для которых вы обычно храните String, int или т. Д., Вы можете использовать привязку данных, чтобы представления отображали ее напрямую, поэтому не нужно хранить значение внутри ViewModel.

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

Джеки
источник
1
Насколько я понимаю, включение специфичного для Android кода требует запуска инструментальных тестов, которые намного медленнее, чем простые тесты JUnit. В настоящее время я использую привязку данных для методов щелчка, но я не понимаю, как это поможет получить ресурсы из xml или для настроек. Я просто понял, что для предпочтений мне также понадобится контекст внутри моей модели. В настоящее время я использую Dagger для внедрения контекста приложения (модуль контекста получает его из статического метода внутри класса приложения)
Винсент Уильямс
@VincentWilliams Да, использование ViewModel помогает абстрагировать ваш код от компонентов пользовательского интерфейса, что упрощает вам проведение тестирования. Но я говорю о том, что основная причина отказа от включения контекста, представлений и т. П. Не из-за причин тестирования, а из-за жизненного цикла ViewModel, который может помочь вам избежать сбоев и других ошибок. Что касается привязки данных, это может помочь вам с ресурсами, потому что большую часть времени, которое вам нужно для доступа к ресурсам в коде, связано с необходимостью применения этой String, color, dimen в вашем макете, что привязка данных может делать напрямую.
Джеки
О, хорошо, я понимаю, что вы имеете в виду, но привязка данных мне не поможет в этом случае, так как мне нужен доступ к строкам для использования в модели (они могут быть помещены в класс констант вместо xml, я полагаю), а также для инициализации SharedPreferences
Винсент Уильямс
3
Если я хочу переключить текст в текстовом представлении на основе модели представления формы значения, строка должна быть локализована, поэтому мне нужны ресурсы в моей модели представления без контекста, как я могу получить доступ к ресурсам?
Сришти Рой
3
@SrishtiRoy Если вы используете привязку данных, легко можно переключать текст TextView на основе значения из вашей модели просмотра. Нет необходимости в доступе к Context внутри вашей ViewModel, потому что все это происходит в файлах макета. Однако, если вы должны использовать Context в своей ViewModel, вам следует рассмотреть возможность использования AndroidViewModel вместо ViewModel. AndroidViewModel содержит контекст приложения, который вы можете вызвать с помощью getApplication (), так что он должен удовлетворить ваши потребности в контексте, если для вашей модели просмотра требуется контекст.
Джеки
15

Краткий ответ - не делайте этого

Зачем ?

Это сводит на нет всю цель просмотра моделей

Почти все, что вы можете делать в модели представления, можно сделать в действии / фрагменте с помощью экземпляров LiveData и различных других рекомендуемых подходов.

humble_wolf
источник
21
Почему тогда вообще существует класс AndroidViewModel?
Alex
1
@AlexBerdnikov Цель MVVM - изолировать представление (Activity / Fragment) от ViewModel даже в большей степени, чем MVP. Так что тестировать будет легче.
hushed_voice
3
@free_style Спасибо за разъяснения, но вопрос все еще стоит: если мы не должны сохранять контекст во ViewModel, почему класс AndroidViewModel вообще существует? Вся его цель - предоставить контекст приложения, не так ли?
Alex
6
@AlexBerdnikov Использование контекста Activity внутри модели просмотра может вызвать утечку памяти. Таким образом, при использовании класса AndroidViewModel вам будет предоставлен Application Context, который (надеюсь) не будет вызывать утечку памяти. Поэтому использование AndroidViewModel может быть лучше, чем передача ему контекста активности. Но все же это затруднит тестирование. Это мой взгляд на это.
hushed_voice
1
Я не могу получить доступ к файлу из папки res / raw из репозитория?
Fugogugo
14

То, что я закончил, вместо того, чтобы иметь контекст непосредственно в ViewModel, я создал классы поставщиков, такие как ResourceProvider, которые предоставили бы мне необходимые ресурсы, и эти классы поставщиков были введены в мою ViewModel.

Винсент Уильямс
источник
1
Я использую ResourcesProvider с Dagger в AppModule. Это хороший подход для получения контекста для ResourcesProvider или AndroidViewModel, лучше ли получить контекст для ресурсов?
Усман Рана
@Vincent: Как использовать resourceProvider, чтобы получить Drawable внутри ViewModel?
HoangVu
@Vegeta Вы бы добавили такой метод, как getDrawableRes(@DrawableRes int id)внутри класса ResourceProvider
Винсент Уильямс,
1
Это противоречит подходу чистой архитектуры, который гласит, что зависимости фреймворка не должны выходить за рамки логики предметной области (ViewModels).
IgorGanapolsky
1
Виртуальные машины @IgorGanapolsky не совсем логика предметной области. Логика домена - это и другие классы, такие как интеракторы и репозитории. ВМ попадают в категорию «клеящих», поскольку они взаимодействуют с вашим доменом, но не напрямую. Если ваши виртуальные машины являются частью вашего домена, вам следует пересмотреть то, как вы используете шаблон, поскольку вы возлагаете на них слишком большую ответственность.
Мрадзинский,
8

TL; DR: вставьте контекст приложения через Dagger в ваши модели просмотра и используйте его для загрузки ресурсов. Если вам нужно загрузить изображения, передайте экземпляр View через аргументы из методов привязки данных и используйте этот контекст View.

MVVM - это хорошая архитектура, и это определенно будущее разработки под Android, но есть пара вещей, которые все еще остаются зелеными. Возьмем, к примеру, обмен данными между уровнями в архитектуре MVVM. Я видел, как разные разработчики (очень известные разработчики) использовали LiveData для передачи различных уровней разными способами. Некоторые из них используют LiveData для связи ViewModel с пользовательским интерфейсом, но затем они используют интерфейсы обратного вызова для связи с репозиториями, или у них есть Interactors / UseCases, и они используют LiveData для связи с ними. Точка здесь, является то , что не все 100% определить еще .

При этом мой подход к вашей конкретной проблеме заключается в том, что контекст приложения доступен через DI для использования в моих ViewModels, чтобы получить такие вещи, как String из моего strings.xml

Если я имею дело с загрузкой изображений, я пытаюсь пройти через объекты View из методов адаптера Databinding и использовать контекст View для загрузки изображений. Зачем? потому что некоторые технологии (например, Glide) могут столкнуться с проблемами, если вы используете контекст приложения для загрузки изображений.

Надеюсь, это поможет!

4gus71n
источник
5
TL; DR должен быть наверху
Жак Кортс
1
Спасибо за ваш ответ. Однако зачем вам использовать кинжал для внедрения контекста, если вы можете расширить свою модель просмотра от androidviewmodel и использовать встроенный контекст, который предоставляет сам класс? Особенно с учетом смехотворного количества шаблонного кода, заставляющего dagger и MVVM работать вместе, другое решение кажется намного более ясным. Что вы думаете об этом?
Josip
7

Как уже упоминалось, AndroidViewModelвы можете получить приложение, Contextно из того, что я собираю в комментариях, вы пытаетесь манипулировать @drawables изнутри, ViewModelчто побеждает цель MVVM.

В общем, необходимость иметь Contextв вашем ViewModelпочти всегда предполагает, что вам следует подумать о переосмыслении того, как вы разделяете логику между вашими Views и ViewModels.

Вместо того, чтобы ViewModelразрешать чертежи и передавать их в Activity / Fragment, подумайте о том, чтобы Fragment / Activity манипулировал чертежами на основе данных, которыми обладает ViewModel. Скажем, вам нужны разные чертежи, которые будут отображаться в представлении для состояния включения / выключения - это то, ViewModelчто должно содержать (возможно, логическое) состояние, но Viewзадача пользователя - выбрать соответствующий объект для рисования.

С DataBinding это можно сделать довольно просто :

<ImageView
...
app:src="@{viewModel.isOn ? @drawable/switch_on : @drawable/switch_off}"
/>

Если у вас больше состояний и чертежей, чтобы избежать громоздкой логики в файле макета, вы можете написать собственный BindingAdapter, который преобразует, скажем, Enumзначение в R.drawable.*(например, масти карт)

Или, может быть, вам нужен Contextдля какого-то компонента, который вы используете в своем ViewModel- тогда создайте компонент вне ViewModelи передайте его. Вы можете использовать DI или синглтоны, или создать Context-зависимый компонент прямо перед инициализацией ViewModelin Fragment/ Activity.

Зачем беспокоиться: Contextэто специфическая вещь для Android, и зависимость от них в ViewModels - плохая практика: они мешают модульному тестированию. С другой стороны, ваши собственные интерфейсы компонентов / сервисов полностью под вашим контролем, поэтому вы можете легко смоделировать их для тестирования.

Иван Барцов
источник
5

имеет ссылку на контекст приложения, но содержит специфичный для Android код

Хорошие новости, вы можете использовать Mockito.mock(Context.class)и заставить контекст возвращать все, что хотите, в тестах!

Поэтому просто используйте a, ViewModelкак обычно, и дайте ему ApplicationContext через ViewModelProviders.Factory, как обычно.

EpicPandaForce
источник
3

вы можете получить доступ к контексту приложения getApplication().getApplicationContext()из ViewModel. Это то, что вам нужно для доступа к ресурсам, настройкам и т. Д.

Алессандро Кругнола
источник
Я предполагаю сузить свой вопрос. Плохо ли иметь контекстную ссылку внутри модели просмотра (не влияет ли это на тестирование?) И будет ли использование класса AndroidViewModel каким-либо образом влиять на Dagger? Разве это не связано с жизненным циклом деятельности? Я использую Dagger для управления жизненным циклом компонентов,
Винсент Уильямс
14
У ViewModelкласса нет getApplicationметода.
beroal
4
Нет, но AndroidViewModelделает
4Oh4
1
Но вам нужно передать экземпляр Application в его конструктор, это то же самое, что получить доступ к экземпляру Application из него
Джон Сардинья
2
Наличие контекста приложения не составляет большой проблемы. Вы не хотите иметь контекст активности / фрагмента, потому что вы потерпели неудачу, если фрагмент / активность уничтожен, а модель представления все еще имеет ссылку на несуществующий контекст. Но вы никогда не потеряете контекст APPLICATION, но у виртуальной машины все еще есть ссылка на него. Правильно? Можете ли вы представить себе сценарий, при котором ваше приложение выходит, а Viewmodel - нет? :)
user1713450
3

Вы не должны использовать объекты, связанные с Android, в своей ViewModel, поскольку мотивом использования ViewModel является разделение кода Java и кода Android, чтобы вы могли тестировать свою бизнес-логику отдельно, и у вас будет отдельный уровень компонентов Android и бизнес-логика. и данные, у вас не должно быть контекста в вашей ViewModel, поскольку это может привести к сбоям

Рохит Шарма
источник
2
Это справедливое наблюдение, но некоторые из внутренних библиотек по-прежнему требуют контекстов приложения, например MediaStore. Ответ 4gus71n ниже объясняет, как идти на компромисс.
Брайан В. Вагнер
1
Да, вы можете использовать контекст приложения, но не контекст действий, поскольку контекст приложения живет на протяжении всего жизненного цикла приложения, но не контекст действия, поскольку передача контекста активности любому асинхронному процессу может привести к утечкам памяти. Контекст, упомянутый в моем сообщении, - это активность Контекст. Но вы все равно должны позаботиться о том, чтобы не передавать контекст в какой-либо асинхронный процесс, даже если это контекст приложения.
Рохит Шарма
2

У меня возникли проблемы SharedPreferencesс использованием ViewModelкласса, поэтому я последовал совету из ответов выше и использовал следующее AndroidViewModel. Теперь все выглядит отлично

Для AndroidViewModel

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;

import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.preference.PreferenceManager;

public class HomeViewModel extends AndroidViewModel {

    private MutableLiveData<String> some_string;

    public HomeViewModel(Application application) {
        super(application);
        some_string = new MutableLiveData<>();
        Context context = getApplication().getApplicationContext();
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(context);
        some_string.setValue("<your value here>"));
    }

}

И в Fragment

import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProviders;


public class HomeFragment extends Fragment {


    public View onCreateView(@NonNull LayoutInflater inflater,
                             ViewGroup container, Bundle savedInstanceState) {
        final View root = inflater.inflate(R.layout.fragment_home, container, false);
        HomeViewModel homeViewModel = ViewModelProviders.of(this).get(HomeViewModel.class);
        homeViewModel.getAddress().observe(getViewLifecycleOwner(), new Observer<String>() {
            @Override
            public void onChanged(@Nullable String address) {


            }
        });
        return root;
    }
}
Дэйвджоэм
источник
0

Я создал это так:

@Module
public class ContextModule {

    @Singleton
    @Provides
    @Named("AppContext")
    public Context provideContext(Application application) {
        return application.getApplicationContext();
    }
}

А затем я просто добавил в AppComponent ContextModule.class:

@Component(
       modules = {
                ...
               ContextModule.class
       }
)
public interface AppComponent extends AndroidInjector<BaseApplication> {
.....
}

А затем я ввел контекст в свою ViewModel:

@Inject
@Named("AppContext")
Context context;
loopidio
источник
0

Используйте следующий шаблон:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}
ЭхсанФаллахи
источник