Я пытаюсь реализовать шаблон MVVM в своем приложении для Android. Я читал, что ViewModels не должен содержать специального кода для Android (чтобы упростить тестирование), однако мне нужно использовать контекст для различных вещей (получение ресурсов из xml, инициализация настроек и т. д.). Как лучше всего это сделать? Я видел, что AndroidViewModel имеет ссылку на контекст приложения, однако он содержит код, специфичный для Android, поэтому я не уверен, что это должно быть в ViewModel. Также они связаны с событиями жизненного цикла Activity, но я использую кинжал для управления набором компонентов, поэтому я не уверен, как это повлияет на это. Я новичок в шаблоне MVVM и Dagger, поэтому приветствую любую помощь!
Вы не должны использовать Context в ViewModel, вместо этого создайте UseCase, чтобы получить контекст таким образом
@RubenCaster, у вас есть образец или ссылка на GitHub?
@Parmesh Нет, извини. Это частный проект = (
вы можете получить доступ к контексту приложения из getApplication().getApplicationContext() из ViewModel. Это то, что вам нужно для доступа к ресурсам, настройкам и т. д.
Я предполагаю сузить свой вопрос. Плохо ли иметь контекстную ссылку внутри модели представления (не влияет ли это на тестирование?) И будет ли использование класса AndroidViewModel каким-либо образом влиять на Dagger? Разве это не связано с жизненным циклом деятельности? Я использую Dagger для управления жизненным циклом компонентов
Класс ViewModel не имеет метода getApplication.
Нет, но AndroidViewModel делает
Но вам нужно передать экземпляр приложения в его конструктор, это то же самое, что получить доступ к экземпляру приложения из него.
Наличие контекста приложения не составляет большой проблемы. Вы не хотите иметь контекст активности / фрагмента, потому что вы столкнетесь с проблемой, если фрагмент / действие будет уничтожено, а модель представления все еще имеет ссылку на теперь несуществующий контекст. Но вы никогда не потеряете контекст APPLICATION, но у виртуальной машины все еще есть ссылка на него. Верно? Можете ли вы представить себе сценарий, при котором ваше приложение завершается, а Viewmodel - нет? :)
Дело не в том, что модели ViewModels не должны содержать специфический для Android код, чтобы упростить тестирование, поскольку это абстракция, которая упрощает тестирование.
Причина, по которой модели представления не должны содержать экземпляр контекста или что-то вроде представлений или других объектов, которые хранятся в контексте, заключается в том, что у него отдельный жизненный цикл, чем у действий и фрагментов.
Я имею в виду, что вы меняете ротацию в своем приложении. Это приводит к тому, что ваша активность и фрагмент разрушаются, поэтому они воссоздают себя. ViewModel предназначен для сохранения в этом состоянии, поэтому есть вероятность сбоев и других исключений, если он все еще удерживает View или Context для уничтоженной Activity.
Что касается того, как вы должны делать то, что хотите делать, MVVM и ViewModel действительно хорошо работают с компонентом привязки данных JetPack. Для большинства вещей, для которых вы обычно храните String, int и т. д., Вы можете использовать привязку данных, чтобы представления отображали ее напрямую, поэтому не нужно хранить значение внутри ViewModel.
Но если вам не нужна привязка данных, вы все равно можете передать контекст внутри конструктора или методов для доступа к ресурсам. Просто не храните экземпляр этого контекста внутри своей модели просмотра.
Насколько я понимаю, включение специфичного для Android кода требует запуска инструментальных тестов, которые намного медленнее, чем простые тесты JUnit. В настоящее время я использую привязку данных для методов щелчка, но я не понимаю, как это поможет получить ресурсы из xml или для предпочтений. Я просто понял, что для предпочтений мне также понадобится контекст внутри моей модели. В настоящее время я использую Dagger для внедрения контекста приложения (модуль контекста получает его из статического метода внутри класса приложения)
@VincentWilliams. Да, использование ViewModel помогает абстрагировать код от компонентов пользовательского интерфейса, что упрощает проведение тестирования. Но я говорю, что основная причина отказа от включения контекста, представлений и т. П. Не из-за причин тестирования, а из-за жизненного цикла ViewModel, который может помочь вам избежать сбоев и других ошибок. Что касается привязки данных, это может помочь вам с ресурсами, потому что большую часть времени, которое вам нужно для доступа к ресурсам в коде, связано с необходимостью применения этой String, color, dimen в вашем макете, что привязка данных может делать напрямую.
О, хорошо, я понимаю, что вы имеете в виду, но привязка данных мне не поможет в этом случае, так как мне нужен доступ к строкам для использования в модели (они могут быть помещены в класс констант вместо xml, я полагаю), а также для инициализации SharedPreferences
Если я хочу переключить текст в текстовом представлении на основе модели представления значения, строка должна быть локализована, поэтому мне нужны ресурсы в моей модели представления, без контекста, как я могу получить доступ к ресурсам?
@SrishtiRoy Если вы используете привязку данных, легко можно переключить текст TextView на основе значения из вашей модели просмотра. Нет необходимости в доступе к Context внутри вашей ViewModel, потому что все это происходит в файлах макета. Однако, если вы должны использовать Context в своей ViewModel, вам следует рассмотреть возможность использования AndroidViewModel вместо ViewModel. AndroidViewModel содержит контекст приложения, который вы можете вызвать с помощью getApplication (), поэтому он должен удовлетворить ваши потребности в контексте, если для вашей модели просмотра требуется контекст.
@Jackey, Главное назначение ViewModel теряется, если Views нужно воссоздавать. ViewModel должен быть исправлен, чтобы мы могли сохранять ссылки на Views и обновлять их с помощью нового Activity.
@Pacerier Вы неправильно поняли основную цель ViewModel. Это проблема разделения проблем. ViewModel не должен хранить ссылки на какие-либо представления, поскольку он отвечает за поддержание данных, которые отображаются на уровне представления. Компоненты пользовательского интерфейса, также известные как представления, поддерживаются слоем представления, и система Android воссоздает представления, если это необходимо. Сохранение ссылки на старые представления будет конфликтовать с этим поведением и вызовет утечку памяти.
TL; DR: вставьте контекст приложения через Dagger в ваши модели просмотра и используйте его для загрузки ресурсов. Если вам нужно загрузить изображения, передайте экземпляр View через аргументы из методов привязки данных и используйте этот контекст View.
MVVM - хорошая архитектура, и это определенно будущее Android-разработки, но есть пара вещей, которые все еще остаются зелеными. Возьмем, к примеру, обмен данными между уровнями в архитектуре MVVM. Я видел, как разные разработчики (очень известные разработчики) использовали LiveData для связи различных уровней по-разному. Некоторые из них используют LiveData для связи ViewModel с пользовательским интерфейсом, но затем они используют интерфейсы обратного вызова для связи с репозиториями, или у них есть Interactors / UseCases, и они используют LiveData для связи с ними. Дело здесь в том, что не все на 100% определяют еще.
При этом мой подход к вашей конкретной проблеме заключается в наличии контекста приложения, доступного через DI для использования в моих ViewModels, чтобы получить такие вещи, как String из моего strings.xml
Если я имею дело с загрузкой изображений, я пытаюсь передать объекты View из методов адаптера привязки данных и использовать контекст View для загрузки изображений. Почему? потому что некоторые технологии (например, Glide) могут столкнуться с проблемами, если вы используете контекст приложения для загрузки изображений.
Надеюсь, поможет!
TL; DR должен быть вверху
Спасибо за ваш ответ. Однако зачем вам использовать кинжал для внедрения контекста, если вы можете расширить свою модель просмотра от androidviewmodel и использовать встроенный контекст, который предоставляет сам класс? Особенно с учетом смехотворного количества шаблонного кода, заставляющего кинжал и MVVM работать вместе, другое решение кажется намного более ясным. Что вы думаете об этом?
Вы не должны использовать объекты, связанные с Android, в своей ViewModel, поскольку мотивом использования ViewModel является разделение кода Java и кода Android, чтобы вы могли тестировать свою бизнес-логику отдельно, и у вас будет отдельный уровень компонентов Android и бизнес-логика. и данные, у вас не должно быть контекста в вашей ViewModel, так как это может привести к сбоям
Это справедливое наблюдение, но для некоторых серверных библиотек по-прежнему требуются контексты приложения, например MediaStore. Ответ 4gus71n ниже объясняет, как идти на компромисс.
Да, вы можете использовать контекст приложения, но не контекст действий, поскольку контекст приложения живет на протяжении всего жизненного цикла приложения, но не контекст действия, поскольку передача контекста активности любому асинхронному процессу может привести к утечкам памяти. Контекст, упомянутый в моем сообщении, - это активность Контекст, но вы все равно должны позаботиться о том, чтобы не передавать контекст в любой асинхронный процесс, даже если это контекст приложения.
has a reference to the application context, however that contains android specific code
Хорошие новости, вы можете использовать Mockito.mock(Context.class) и заставить контекст возвращать все, что вы хотите, в тестах!
Поэтому просто используйте ViewModel, как обычно, и дайте ему ApplicationContext через ViewModelProviders.Factory, как обычно.
То, что я в итоге сделал, вместо того, чтобы иметь контекст непосредственно в ViewModel, я создал классы провайдеров, такие как ResourceProvider, которые предоставили бы мне необходимые ресурсы, и эти классы провайдеров были введены в мою ViewModel.
Я использую ResourcesProvider с Dagger в AppModule. Это хороший подход для получения контекста для ResourcesProvider или AndroidViewModel, лучше для получения контекста для ресурсов?
@Vincent: Как использовать resourceProvider, чтобы получить Drawable внутри ViewModel?
@Vegeta Вы бы добавили такой метод, как getDrawableRes(@DrawableRes int id), внутри класса ResourceProvider
Это противоречит подходу чистой архитектуры, который гласит, что зависимости фреймворка не должны выходить за рамки логики предметной области (ViewModels).
Виртуальные машины @IgorGanapolsky не совсем логика предметной области. Логика домена - это и другие классы, такие как интеракторы и репозитории. ВМ попадают в категорию «клей», поскольку они взаимодействуют с вашим доменом, но не напрямую. Если ваши виртуальные машины являются частью вашего домена, вам следует пересмотреть то, как вы используете шаблон, поскольку вы возлагаете на них слишком большую ответственность.
@VincentWilliams Не могли бы вы прояснить - поэтому ResourceProvider имеет ссылку на Context, а ViewModel, в свою очередь, имеет ссылку на ResourceProvider, чем это отличается от ссылки контекста в ViewModel?
Вы можете использовать контекст Application, который предоставляется AndroidViewModel, вы должны расширить AndroidViewModel, который является просто ViewModel, который включает ссылку на Application.
Для модели представления компонентов архитектуры Android,
Не рекомендуется передавать контекст действия в ViewModel действия, поскольку это утечка памяти.
Следовательно, чтобы получить контекст в вашей ViewModel, класс ViewModel должен расширять класс Модель просмотра Android. Таким образом вы можете получить контекст, как показано в примере кода ниже.
class ActivityViewModel(application: Application) : AndroidViewModel(application) {
private val context = getApplication<Application>().applicationContext
//... ViewModel methods
}
Почему бы не использовать напрямую параметр приложения и обычную ViewModel? Не вижу смысла в "getApplication <Application> ()". Он просто добавляет шаблон.
Почему это будет утечка памяти?
Понятно, потому что активность будет уничтожаться чаще, чем ее модель представления (например, когда экран вращается). К сожалению, сборщик мусора не освободит память, потому что модель представления все еще имеет ссылку на нее.
Быстрый вопрос: мы можем просто использовать переменную application. Есть ли смысл использовать getApplication<Application>() вместо использования application, переданного в ActivityViewModel? На самом деле они в любом случае являются одним и тем же приложением.
@TheincredibleJan Я пробовал, но не работает. Почему-то ViewModel не может быть создан. Но это работает, если мы используем AndroidViewModel вместо ViewModel. Я предполагаю, что внедрение зависимостей с помощью ViewModelProvider не работает, если мы используем ViewModel.
В любом случае, сохранение ссылки на старую активность в ViewModel - это не просто утечка памяти, это просто ошибочный код. Когда ViewModel вызывает ссылку на свою активность, она могла быть убита давным-давно → CRASH. Поэтому не храните действия в ViewModels, потому что у действия другой жизненный цикл. @ Ответ Джеки лучше, чем этот.
Это определенно утечка памяти.
Как уже упоминали другие, есть AndroidViewModel, от которого вы можете получить приложение Context, но из того, что я собираю в комментариях, вы пытаетесь манипулировать @drawable из своего ViewModel, что побеждает цель MVVM.
В общем, необходимость иметь Context в вашем ViewModel почти всегда предполагает, что вам следует подумать о переосмыслении того, как вы разделяете логику между вашими View и 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.*, например:
enum class CatType { NYAN, GRUMPY, LOL }
class CatViewModel {
val catType: LiveData<CatType> = ...
// View-tier logic, takes the burden of knowing
// Contexts and R.** refs from the ViewModel
@BindingAdapter("bindCatImage")
fun bindCatImage(view: ImageView, catType: CatType) = view.apply {
val resource = when (value) {
CatType.NYAN -> R.drawable.cat_nyan
CatType.GRUMPY -> R.drawable.cat_grumpy
CatType.LOL -> R.drawable.cat_lol
}
setImageResource(resource)
}
<ImageView
bindCatType = "@{vm.catType}"
... />
Если вам нужен Context для некоторого компонент, который вы используете в вашего ViewModel - тогда создайте компонент вне ViewModel и передайте его. Вы можете использовать DI или синглтоны, или создать зависимый от Context компонент прямо перед инициализацией ViewModel в Fragment / Activity.
Context - это специфическая вещь для Android, и в зависимости от нее в ViewModels он громоздок для модульных тестов (конечно, вы можете использовать AndroidJunitRunner для специфичных для Android вещей, но имеет смысл иметь более чистый код без дополнительной зависимости). Если вы не зависите от Context, имитировать все для теста ViewModel проще. Итак, практическое правило: не используйте Context во ViewModels, если у вас нет для этого веской причины.
Краткий ответ - не делайте этого
Почему ?
Это сводит на нет всю цель просмотра моделей
Практически все, что вы можете сделать в модели представления, можно сделать в действии / фрагменте, используя экземпляры LiveData и различные другие рекомендуемые подходы.
Почему тогда вообще существует класс AndroidViewModel?
@AlexBerdnikov Цель MVVM - изолировать представление (Activity / Fragment) от ViewModel даже в большей степени, чем MVP. Так что тестировать будет легче.
@free_style Спасибо за разъяснения, но вопрос все еще остается в силе: если мы не должны сохранять контекст во ViewModel, почему класс AndroidViewModel вообще существует? Вся его цель - предоставить контекст приложения, не так ли?
@AlexBerdnikov Использование контекста Activity внутри модели просмотра может вызвать утечку памяти. Таким образом, при использовании класса AndroidViewModel вам будет предоставлен Application Context, который (надеюсь) не будет вызывать утечку памяти. Поэтому использование AndroidViewModel может быть лучше, чем передача ему контекста активности. Но все же это затруднит тестирование. Это мой взгляд на это.
@AlexBerdnikov Цель класса ViewModel - хранить данные о вашей активности, чтобы их можно было использовать для деятельности (или ее фрагментов) более надежным способом. developer.android.com/topic/libraries/architecture/viewmodel
Я не могу получить доступ к файлу из папки res / raw из репозитория?
обратите внимание, что это не контекст активности, а контекст приложения, работает только одно приложение, которое уничтожает при завершении приложения, поэтому в этом конкретном случае нет возможности утечки памяти.
Android ViewModels НЕ являются «моделями просмотра». Их цель - восполнить архитектурные пробелы в действиях / фрагментах, теряющих свое состояние при изменении вращения / конфигурации, что было ошибкой для Android с момента его создания. Отношение к ним так, как если бы они были НАМЕРЕННОЙ отдельной абстракцией, только добавляет кучу дополнительной сложности с нулевой реальной пользой (фактически наоборот). В Android Activity + Fragment + ViewModel действуют как «контроллер». Нет веской причины, по которой ViewModel следует изолировать от контекста. Логически это действие / фрагмент.
Я создал это так:
@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;
Используйте следующий шаблон:
class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
body...
}
У меня возникли проблемы с получением 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;
}
}
Проблема с внедрением контекста в ViewModel заключается в том, что контекст может измениться в любое время, в зависимости от поворота экрана, ночного режима или языка системы, и любые возвращенные ресурсы могут измениться соответствующим образом. Возврат простого идентификатора ресурса вызывает проблемы с дополнительными параметрами, такими как подстановки getString. Возврат высокоуровневого результата и перенос логики рендеринга в Activity затрудняют тестирование.
Мое решение состоит в том, чтобы ViewModel генерировал и возвращал функцию, которая позже запускалась через контекст Activity. Синтаксический сахар Kotlin делает это невероятно простым!
ViewModel.kt:
// connectedStatus holds a function that calls Context methods
// `this` can be elided
val connectedStatus = MutableLiveData<Context.() -> String> {
// initial value
this.getString(R.string.connectionStatusWaiting)
}
connectedStatus.postValue {
this.getString(R.string.connectionStatusConnected, brand)
}
Activity.kt // is a Context
override fun onCreate(_: Bundle?) {
connectionViewModel.connectedStatus.observe(this) { it ->
// runs the posted value with the given Context receiver
txtConnectionStatus.text = this.run(it)
}
}
Это позволяет ViewModel хранить всю логику для вычисления отображаемой информации, проверенной модульными тестами, при этом Activity является очень простым представлением без внутренней логики для скрытия ошибок.
А чтобы включить поддержку привязки данных, вы просто добавляете простой BindingAdapter, например: @BindingAdapter("android:text")fun setText(view: TextView, value: Context.() -> String) {view.text = view.context.run(value)}
Использование рукояти
@Module
@InstallIn(SingletonComponent::class)
class AppModule {
@Singleton
@Provides
fun provideContext(application: Application): Context = application.applicationContext
}
Затем передайте его через конструктор
class MyRepository @Inject constructor(private val context: Context) {
...
}
Насколько вообще актуален Hilt? Это не похоже на то, что Hilt волшебным образом предоставляет контекст, вы могли бы сделать это и без Hilt.
На всякий случай, если кто-то пытается использовать
AndroidViewModel, но получаетCannot create instance exception, вы можете обратиться к моему этому ответу stackoverflow.com/a/62626408/1055241