Я пытаюсь реализовать шаблон MVVM в своем приложении для Android. Я читал, что ViewModels не должен содержать специального кода для Android (чтобы упростить тестирование), однако мне нужно использовать контекст для различных вещей (получение ресурсов из xml, инициализация настроек и т. Д.). Как лучше всего это сделать? Я видел, что в нем AndroidViewModel
есть ссылка на контекст приложения, но он содержит код, специфичный для Android, поэтому я не уверен, что это должно быть в ViewModel. Также они связаны с событиями жизненного цикла Activity, но я использую кинжал для управления набором компонентов, поэтому я не уверен, как это повлияет на это. Я новичок в шаблоне MVVM и Dagger, поэтому приветствую любую помощь!
android
mvvm
dagger-2
android-context
Винсент Уильямс
источник
источник
AndroidViewModel
но получает его,Cannot create instance exception
вы можете обратиться к моему этому ответу stackoverflow.com/a/62626408/1055241Ответы:
Вы можете использовать
Application
контекст, который предоставляетсяAndroidViewModel
, вы должны расширить,AndroidViewModel
который простоViewModel
включаетApplication
ссылку.источник
Для модели представления компонентов архитектуры Android,
Не рекомендуется передавать свой контекст действия в ViewModel действия, поскольку это утечка памяти.
Следовательно, чтобы получить контекст в вашей ViewModel, класс ViewModel должен расширять класс Android View Model . Таким образом вы можете получить контекст, как показано в примере кода ниже.
class ActivityViewModel(application: Application) : AndroidViewModel(application) { private val context = getApplication<Application>().applicationContext //... ViewModel methods }
источник
Дело не в том, что модели ViewModels не должны содержать специфичный для Android код для упрощения тестирования, поскольку это абстракция, которая упрощает тестирование.
Причина, по которой модели представления не должны содержать экземпляр контекста или что-то вроде представлений или других объектов, которые хранятся в контексте, заключается в том, что у него отдельный жизненный цикл, чем у действий и фрагментов.
Я имею в виду, что вы меняете ротацию в своем приложении. Это приводит к тому, что ваша активность и фрагмент разрушаются, поэтому они воссоздают себя. ViewModel предназначен для сохранения в этом состоянии, поэтому есть вероятность сбоев и других исключений, если он все еще удерживает View или Context для уничтоженной Activity.
Что касается того, как вы должны делать то, что хотите делать, MVVM и ViewModel действительно хорошо работают с компонентом привязки данных JetPack. Для большинства вещей, для которых вы обычно храните String, int или т. Д., Вы можете использовать привязку данных, чтобы представления отображали ее напрямую, поэтому не нужно хранить значение внутри ViewModel.
Но если вам не нужна привязка данных, вы все равно можете передать контекст внутри конструктора или методов для доступа к ресурсам. Просто не храните экземпляр этого контекста внутри своей ViewModel.
источник
Краткий ответ - не делайте этого
Зачем ?
Это сводит на нет всю цель просмотра моделей
Почти все, что вы можете делать в модели представления, можно сделать в действии / фрагменте с помощью экземпляров LiveData и различных других рекомендуемых подходов.
источник
То, что я закончил, вместо того, чтобы иметь контекст непосредственно в ViewModel, я создал классы поставщиков, такие как ResourceProvider, которые предоставили бы мне необходимые ресурсы, и эти классы поставщиков были введены в мою ViewModel.
источник
getDrawableRes(@DrawableRes int id)
внутри класса ResourceProviderTL; DR: вставьте контекст приложения через Dagger в ваши модели просмотра и используйте его для загрузки ресурсов. Если вам нужно загрузить изображения, передайте экземпляр View через аргументы из методов привязки данных и используйте этот контекст View.
MVVM - это хорошая архитектура, и это определенно будущее разработки под Android, но есть пара вещей, которые все еще остаются зелеными. Возьмем, к примеру, обмен данными между уровнями в архитектуре MVVM. Я видел, как разные разработчики (очень известные разработчики) использовали LiveData для передачи различных уровней разными способами. Некоторые из них используют LiveData для связи ViewModel с пользовательским интерфейсом, но затем они используют интерфейсы обратного вызова для связи с репозиториями, или у них есть Interactors / UseCases, и они используют LiveData для связи с ними. Точка здесь, является то , что не все 100% определить еще .
При этом мой подход к вашей конкретной проблеме заключается в том, что контекст приложения доступен через DI для использования в моих ViewModels, чтобы получить такие вещи, как String из моего strings.xml
Если я имею дело с загрузкой изображений, я пытаюсь пройти через объекты View из методов адаптера Databinding и использовать контекст View для загрузки изображений. Зачем? потому что некоторые технологии (например, Glide) могут столкнуться с проблемами, если вы используете контекст приложения для загрузки изображений.
Надеюсь, это поможет!
источник
Как уже упоминалось,
AndroidViewModel
вы можете получить приложение,Context
но из того, что я собираю в комментариях, вы пытаетесь манипулировать@drawable
s изнутри,ViewModel
что побеждает цель MVVM.В общем, необходимость иметь
Context
в вашемViewModel
почти всегда предполагает, что вам следует подумать о переосмыслении того, как вы разделяете логику между вашимиView
s и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
-зависимый компонент прямо перед инициализациейViewModel
inFragment
/Activity
.Зачем беспокоиться:
Context
это специфическая вещь для Android, и зависимость от них вViewModel
s - плохая практика: они мешают модульному тестированию. С другой стороны, ваши собственные интерфейсы компонентов / сервисов полностью под вашим контролем, поэтому вы можете легко смоделировать их для тестирования.источник
Хорошие новости, вы можете использовать
Mockito.mock(Context.class)
и заставить контекст возвращать все, что хотите, в тестах!Поэтому просто используйте a,
ViewModel
как обычно, и дайте ему ApplicationContext через ViewModelProviders.Factory, как обычно.источник
вы можете получить доступ к контексту приложения
getApplication().getApplicationContext()
из ViewModel. Это то, что вам нужно для доступа к ресурсам, настройкам и т. Д.источник
ViewModel
класса нетgetApplication
метода.AndroidViewModel
делаетВы не должны использовать объекты, связанные с Android, в своей ViewModel, поскольку мотивом использования ViewModel является разделение кода Java и кода Android, чтобы вы могли тестировать свою бизнес-логику отдельно, и у вас будет отдельный уровень компонентов Android и бизнес-логика. и данные, у вас не должно быть контекста в вашей ViewModel, поскольку это может привести к сбоям
источник
У меня возникли проблемы
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; } }
источник
Я создал это так:
А затем я просто добавил в AppComponent ContextModule.class:
@Component( modules = { ... ContextModule.class } ) public interface AppComponent extends AndroidInjector<BaseApplication> { ..... }
А затем я ввел контекст в свою ViewModel:
источник
Используйте следующий шаблон:
источник