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

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

На всякий случай, если кто-то пытается использовать AndroidViewModel, но получает Cannot create instance exception, вы можете обратиться к моему этому ответу stackoverflow.com/a/62626408/1055241

gprathour 28.06.2020 20:14

Вы не должны использовать Context в ViewModel, вместо этого создайте UseCase, чтобы получить контекст таким образом

Ruben Caster 28.08.2020 10:46

@RubenCaster, у вас есть образец или ссылка на GitHub?

Parmesh 04.07.2021 04:56

@Parmesh Нет, извини. Это частный проект = (

Ruben Caster 05.07.2021 12:50
115
4
98 012
15
Перейти к ответу Данный вопрос помечен как решенный

Ответы 15

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

Я предполагаю сузить свой вопрос. Плохо ли иметь контекстную ссылку внутри модели представления (не влияет ли это на тестирование?) И будет ли использование класса AndroidViewModel каким-либо образом влиять на Dagger? Разве это не связано с жизненным циклом деятельности? Я использую Dagger для управления жизненным циклом компонентов

Vincent Williams 21.07.2018 02:51

Класс ViewModel не имеет метода getApplication.

beroal 01.01.2019 14:50

Нет, но AndroidViewModel делает

4Oh4 16.01.2019 01:11

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

John Sardinha 06.06.2019 11:22

Наличие контекста приложения не составляет большой проблемы. Вы не хотите иметь контекст активности / фрагмента, потому что вы столкнетесь с проблемой, если фрагмент / действие будет уничтожено, а модель представления все еще имеет ссылку на теперь несуществующий контекст. Но вы никогда не потеряете контекст APPLICATION, но у виртуальной машины все еще есть ссылка на него. Верно? Можете ли вы представить себе сценарий, при котором ваше приложение завершается, а Viewmodel - нет? :)

user1713450 27.06.2019 04:16

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

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

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

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

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

Насколько я понимаю, включение специфичного для Android кода требует запуска инструментальных тестов, которые намного медленнее, чем простые тесты JUnit. В настоящее время я использую привязку данных для методов щелчка, но я не понимаю, как это поможет получить ресурсы из xml или для предпочтений. Я просто понял, что для предпочтений мне также понадобится контекст внутри моей модели. В настоящее время я использую Dagger для внедрения контекста приложения (модуль контекста получает его из статического метода внутри класса приложения)

Vincent Williams 21.07.2018 02:49

@VincentWilliams. Да, использование ViewModel помогает абстрагировать код от компонентов пользовательского интерфейса, что упрощает проведение тестирования. Но я говорю, что основная причина отказа от включения контекста, представлений и т. П. Не из-за причин тестирования, а из-за жизненного цикла ViewModel, который может помочь вам избежать сбоев и других ошибок. Что касается привязки данных, это может помочь вам с ресурсами, потому что большую часть времени, которое вам нужно для доступа к ресурсам в коде, связано с необходимостью применения этой String, color, dimen в вашем макете, что привязка данных может делать напрямую.

Jackey 21.07.2018 02:59

О, хорошо, я понимаю, что вы имеете в виду, но привязка данных мне не поможет в этом случае, так как мне нужен доступ к строкам для использования в модели (они могут быть помещены в класс констант вместо xml, я полагаю), а также для инициализации SharedPreferences

Vincent Williams 21.07.2018 03:05

Если я хочу переключить текст в текстовом представлении на основе модели представления значения, строка должна быть локализована, поэтому мне нужны ресурсы в моей модели представления, без контекста, как я могу получить доступ к ресурсам?

Srishti Roy 07.01.2019 09:20

@SrishtiRoy Если вы используете привязку данных, легко можно переключить текст TextView на основе значения из вашей модели просмотра. Нет необходимости в доступе к Context внутри вашей ViewModel, потому что все это происходит в файлах макета. Однако, если вы должны использовать Context в своей ViewModel, вам следует рассмотреть возможность использования AndroidViewModel вместо ViewModel. AndroidViewModel содержит контекст приложения, который вы можете вызвать с помощью getApplication (), поэтому он должен удовлетворить ваши потребности в контексте, если для вашей модели просмотра требуется контекст.

Jackey 07.01.2019 20:24

@Jackey, Главное назначение ViewModel теряется, если Views нужно воссоздавать. ViewModel должен быть исправлен, чтобы мы могли сохранять ссылки на Views и обновлять их с помощью нового Activity.

Pacerier 17.05.2020 22:50

@Pacerier Вы неправильно поняли основную цель ViewModel. Это проблема разделения проблем. ViewModel не должен хранить ссылки на какие-либо представления, поскольку он отвечает за поддержание данных, которые отображаются на уровне представления. Компоненты пользовательского интерфейса, также известные как представления, поддерживаются слоем представления, и система Android воссоздает представления, если это необходимо. Сохранение ссылки на старые представления будет конфликтовать с этим поведением и вызовет утечку памяти.

Jackey 17.05.2020 23:34

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 должен быть вверху

Jacques Koorts 12.09.2019 15:36

Спасибо за ваш ответ. Однако зачем вам использовать кинжал для внедрения контекста, если вы можете расширить свою модель просмотра от androidviewmodel и использовать встроенный контекст, который предоставляет сам класс? Особенно с учетом смехотворного количества шаблонного кода, заставляющего кинжал и MVVM работать вместе, другое решение кажется намного более ясным. Что вы думаете об этом?

Josip Domazet 23.12.2019 00:58

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

Это справедливое наблюдение, но для некоторых серверных библиотек по-прежнему требуются контексты приложения, например MediaStore. Ответ 4gus71n ниже объясняет, как идти на компромисс.

Bryan W. Wagner 17.06.2019 18:25

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

Rohit Sharma 18.06.2019 06:16

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, лучше для получения контекста для ресурсов?

Usman Rana 19.11.2018 08:24

@Vincent: Как использовать resourceProvider, чтобы получить Drawable внутри ViewModel?

Bulma 27.12.2018 10:07

@Vegeta Вы бы добавили такой метод, как getDrawableRes(@DrawableRes int id), внутри класса ResourceProvider

Vincent Williams 28.12.2018 18:24

Это противоречит подходу чистой архитектуры, который гласит, что зависимости фреймворка не должны выходить за рамки логики предметной области (ViewModels).

IgorGanapolsky 23.12.2019 22:36

Виртуальные машины @IgorGanapolsky не совсем логика предметной области. Логика домена - это и другие классы, такие как интеракторы и репозитории. ВМ попадают в категорию «клей», поскольку они взаимодействуют с вашим доменом, но не напрямую. Если ваши виртуальные машины являются частью вашего домена, вам следует пересмотреть то, как вы используете шаблон, поскольку вы возлагаете на них слишком большую ответственность.

mradzinski 21.10.2020 16:47

@VincentWilliams Не могли бы вы прояснить - поэтому ResourceProvider имеет ссылку на Context, а ViewModel, в свою очередь, имеет ссылку на ResourceProvider, чем это отличается от ссылки контекста в ViewModel?

XZen 26.12.2020 14:58

Вы можете использовать контекст 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> ()". Он просто добавляет шаблон.

The incredible Jan 06.10.2020 11:04

Почему это будет утечка памяти?

Ben Butterworth 05.12.2020 19:59

Понятно, потому что активность будет уничтожаться чаще, чем ее модель представления (например, когда экран вращается). К сожалению, сборщик мусора не освободит память, потому что модель представления все еще имеет ссылку на нее.

Ben Butterworth 05.12.2020 20:10

Быстрый вопрос: мы можем просто использовать переменную application. Есть ли смысл использовать getApplication<Application>() вместо использования application, переданного в ActivityViewModel? На самом деле они в любом случае являются одним и тем же приложением.

starriet 24.01.2021 03:42

@TheincredibleJan Я пробовал, но не работает. Почему-то ViewModel не может быть создан. Но это работает, если мы используем AndroidViewModel вместо ViewModel. Я предполагаю, что внедрение зависимостей с помощью ViewModelProvider не работает, если мы используем ViewModel.

starriet 24.01.2021 03:46

В любом случае, сохранение ссылки на старую активность в ViewModel - это не просто утечка памяти, это просто ошибочный код. Когда ViewModel вызывает ссылку на свою активность, она могла быть убита давным-давно → CRASH. Поэтому не храните действия в ViewModels, потому что у действия другой жизненный цикл. @ Ответ Джеки лучше, чем этот.

Ben Butterworth 06.02.2021 16:17

Это определенно утечка памяти.

IgorGanapolsky 24.05.2021 23:04

Как уже упоминали другие, есть 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?

Alex Berdnikov 19.09.2019 23:07

@AlexBerdnikov Цель MVVM - изолировать представление (Activity / Fragment) от ViewModel даже в большей степени, чем MVP. Так что тестировать будет легче.

hushed_voice 20.09.2019 10:59

@free_style Спасибо за разъяснения, но вопрос все еще остается в силе: если мы не должны сохранять контекст во ViewModel, почему класс AndroidViewModel вообще существует? Вся его цель - предоставить контекст приложения, не так ли?

Alex Berdnikov 20.09.2019 19:58

@AlexBerdnikov Использование контекста Activity внутри модели просмотра может вызвать утечку памяти. Таким образом, при использовании класса AndroidViewModel вам будет предоставлен Application Context, который (надеюсь) не будет вызывать утечку памяти. Поэтому использование AndroidViewModel может быть лучше, чем передача ему контекста активности. Но все же это затруднит тестирование. Это мой взгляд на это.

hushed_voice 25.09.2019 12:46

@AlexBerdnikov Цель класса ViewModel - хранить данные о вашей активности, чтобы их можно было использовать для деятельности (или ее фрагментов) более надежным способом. developer.android.com/topic/libraries/architecture/viewmodel

humble_wolf 26.09.2019 14:04

Я не могу получить доступ к файлу из папки res / raw из репозитория?

Fugogugo 18.03.2020 11:38

обратите внимание, что это не контекст активности, а контекст приложения, работает только одно приложение, которое уничтожает при завершении приложения, поэтому в этом конкретном случае нет возможности утечки памяти.

Hooni 04.02.2021 02:38

Android ViewModels НЕ являются «моделями просмотра». Их цель - восполнить архитектурные пробелы в действиях / фрагментах, теряющих свое состояние при изменении вращения / конфигурации, что было ошибкой для Android с момента его создания. Отношение к ним так, как если бы они были НАМЕРЕННОЙ отдельной абстракцией, только добавляет кучу дополнительной сложности с нулевой реальной пользой (фактически наоборот). В Android Activity + Fragment + ViewModel действуют как «контроллер». Нет веской причины, по которой ViewModel следует изолировать от контекста. Логически это действие / фрагмент.

Marchy 08.03.2021 23:10

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

@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)}

hufman 21.12.2020 00:14

Использование рукояти

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

Farid 27.04.2021 21:37

Другие вопросы по теме