Як отримати контекст в Android MVVM ViewModel


90

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


Тільки в разі , якщо хто - то намагається використовувати , AndroidViewModelале отримати Cannot create instance exceptionто ви можете звернутися до цей мій відповідь stackoverflow.com/a/62626408/1055241
gprathour

Ви не повинні використовувати контекст у ViewModel, замість цього створюйте UseCase, щоб отримати контекст таким чином
Рубен Кастер,

Відповіді:


71

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


Працював як оберіг!
SPM

Хтось може це показати в коді? Я на Яві
Бісвас Каярголі,

55

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

Передавати контекст активності у ViewModel Activity не є доброю практикою, оскільки це витік пам’яті.

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

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

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

    //... ViewModel methods 

}

2
Чому б не використовувати безпосередньо параметр програми та звичайний ViewModel? Я не бачу сенсу в "getApplication <Application> ()". Це просто додає шаблон.
Неймовірний

50

Це не те, що ViewModels не повинні містити спеціальний код для Android, щоб полегшити тестування, оскільки саме абстракція полегшує тестування.

Причина, чому ViewModels не повинна містити екземпляр контексту або щось подібне до подань або інших об'єктів, що містять контекст, полягає в тому, що він має окремий життєвий цикл, ніж "Дії" та "Фрагменти".

Що я маю на увазі під цим, скажімо, ви змінюєте обертання у своєму додатку. Це призводить до того, що ваша активність і фрагмент знищуються, тому вони відтворюються. ViewModel призначений для збереження під час цього стану, тому є ймовірність аварійного завершення роботи та інших винятків, якщо він все ще містить перегляд або контекст знищеної активності.

Що стосується того, як вам слід робити те, що ви хочете зробити, MVVM і ViewModel дуже добре працюють з компонентом Databinding JetPack. Для більшості речей, для яких ти зазвичай зберігаєш String, int або ін., Ти можеш використовувати Databinding, щоб змусити подання відображати його безпосередньо, тому не потрібно зберігати значення всередині ViewModel.

Але якщо ви не хочете прив'язки даних, ви все одно можете передати контекст всередині конструктора або методів доступу до ресурсів. Просто не тримайте екземпляр цього контексту всередині вашого ViewModel.


1
Я зрозумів, що для включення коду, специфічного для Android, потрібні інструментальні тести, які виконуються набагато повільніше, ніж звичайні тести JUnit. В даний час я використовую прив'язку даних для методів клацання, але я не розумію, як це допомогло б отримати ресурси з xml або для налаштувань. Я щойно зрозумів, що для уподобань мені також знадобиться контекст всередині моєї моделі. Зараз я роблю, щоб Кинджал вводив контекст програми (модуль контексту отримує його із статичного методу всередині класу програми)
Вінсент Вільямс,

@VincentWilliams Так, використання ViewModel допомагає абстрагувати ваш код від компонентів інтерфейсу, що полегшує вам тестування. Але те, що я кажу, полягає в тому, що основна причина не включення будь-якого контексту, переглядів тощо не через причини тестування, а через життєвий цикл ViewModel, який може допомогти вам уникнути збоїв та інших помилок. Що стосується прив'язки даних, це може допомогти вам із ресурсами, оскільки більшу частину часу, необхідного для доступу до ресурсів у коді, пов'язано з необхідністю застосувати цей рядок, колір, розмір у своєму макеті, що прив'язка даних може зробити безпосередньо.
Jackey

О гаразд, я розумію, що ви маєте на увазі, але прив'язка даних мені в цьому випадку не допоможе, оскільки мені потрібно отримати доступ до рядків для використання в моделі (їх, можливо, можна помістити в клас констант замість xml), а також для ініціалізації SharedPreferences
Вінсент Вільямс

3
якщо я хочу перемикати текст у текстовому поданні на основі viewmodel форми значення, рядок потрібно локалізувати, тому мені потрібно отримати ресурси в моїй viewmodel без контексту, як я отримаю доступ до ресурсів?
Шрішті Рой

3
@SrishtiRoy Якщо ви використовуєте прив'язку даних, легко переключити текст TextView на основі значення з вашої моделі перегляду. Немає потреби в доступі до контексту у вашому ViewModel, оскільки все це відбувається у файлах макета. Однак якщо вам потрібно використовувати контекст у вашому ViewModel, то вам слід розглянути можливість використання AndroidViewModel замість ViewModel. AndroidViewModel містить контекст програми, який ви можете викликати за допомогою getApplication (), так що він повинен задовольнити ваші потреби в контексті, якщо ваш ViewModel вимагає контексту.
Jackey

15

Коротка відповідь - не робіть цього

Чому?

Це перешкоджає усьому призначенню моделей перегляду

Майже все, що ви можете зробити в моделі перегляду, можна зробити у дії / фрагменті, використовуючи екземпляри LiveData та різні інші рекомендовані підходи.


21
Чому тоді клас AndroidViewModel взагалі існує?
Алекс Бердников

1
@AlexBerdnikov Мета MVVM - ізолювати вигляд (Activity / Fragment) від ViewModel навіть більше, ніж MVP. Так що буде легше тестувати.
hushed_voice

3
@free_style Дякуємо за роз'яснення, але питання все ще стоїть: якщо ми не повинні зберігати контекст у ViewModel, чому клас AndroidViewModel взагалі існує? Вся його мета - забезпечити контекст програми, чи не так?
Олексій Бердников

6
@AlexBerdnikov Використання контексту Activity усередині моделі перегляду може спричинити витік пам'яті. Отже, за допомогою класу AndroidViewModel ви отримаєте контекст програми, який не буде (сподіваємось) спричиняти витік пам'яті. Тож використання AndroidViewModel може бути кращим, ніж передача йому контексту активності. Але все одно робити це ускладнить тестування. Це мій погляд на це.
hushed_voice

1
Я не можу отримати доступ до файлу з папки res / raw із сховища?
Фугогуго

14

Що я в підсумку робив замість того, щоб мати контекст безпосередньо в ViewModel, я створив класи провайдерів, такі як ResourceProvider, які давали б мені потрібні ресурси, і ці класи провайдерів мені вводили у мій ViewModel


1
Я використовую ResourcesProvider з кинджалом в AppModule. Чи хороший підхід отримати контекст для ResourcesProvider або AndroidViewModel краще отримувати контекст для ресурсів?
Усман Рана,

@Vincent: Як використовувати resourceProvider, щоб отримати Drawable всередині ViewModel?
HoangVu

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

1
Це суперечить підходу чистої архітектури, який говорить, що залежності фреймворку не повинні переходити межі в логіку домену (ViewModels).
ІгорГанапольський

1
Віртуальні машини @IgorGanapolsky - це не зовсім логіка домену. Логіка домену - це інші класи, такі як інтерактори та сховища, щоб назвати декілька. Віртуальні машини потрапляють до категорії "клей", оскільки вони взаємодіють з вашим доменом, але не безпосередньо. Якщо ваші віртуальні машини є частиною вашого домену, вам слід переглянути, як ви використовуєте шаблон, оскільки ви покладаєте на них занадто велику відповідальність.
mradzinski

8

TL; DR: Внесіть контекст Програми через Кинджал у ваші ViewModels і використовуйте його для завантаження ресурсів. Якщо вам потрібно завантажити зображення, передайте екземпляр View через аргументи методів Databinding і використовуйте цей контекст View.

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

З огляду на це, мій підхід до вашої конкретної проблеми полягає в тому, щоб контекст програми був доступний через DI для використання у моїх ViewModels для отримання таких речей, як String з мого strings.xml

Якщо я маю справу із завантаженням зображень, я намагаюся передати об'єкти View з методів адаптера прив'язки даних і використовую контекст View для завантаження зображень. Чому? оскільки деякі технології (наприклад, Glide) можуть зіткнутися з проблемами, якщо ви використовуєте контекст Програми для завантаження зображень.

Сподіваюся, це допоможе!


5
TL; DR повинен бути вгорі
Jacques Koorts

1
Спасибі за вашу відповідь. Однак чому б ви використовували кинджал для ін'єкції контексту, якщо змогли змусити вашу viewmodel розширюватися від androidviewmodel і використовувати вбудований контекст, який надає сам клас? Особливо враховуючи смішну кількість шаблонного коду, щоб змусити кинджал та MVVM працювати разом, інше рішення здається набагато чіткішим imo. Які ваші думки з цього приводу?
Йосип

7

Як вже згадували інші, AndroidViewModelви можете отримати висновок, щоб отримати програму, Contextале з того, що я збираю в коментарях, ви намагаєтесь маніпулювати @drawables з вашого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.* (наприклад, масті карт)

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

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


5

має посилання на контекст програми, однак містить специфічний для Android код

Хороші новини, ви можете використовувати Mockito.mock(Context.class) і змусити контекст повертати все, що завгодно в тестах!

Тож просто використовуйте ViewModelяк зазвичай, і надайте йому ApplicationContext через ViewModelProviders.Factory, як зазвичай.


3

Ви можете отримати доступ до контексту програми із getApplication().getApplicationContext()ViewModel. Це те, що вам потрібно для доступу до ресурсів, уподобань тощо.


Я думаю, щоб звузити своє питання. Чи погано мати посилання на контекст всередині viewmodel (чи це не впливає на тестування?), І чи не вплине використання AndroidViewModel класу на Dagger якимось чином? Хіба це не прив'язано до життєвого циклу діяльності? Я використовую Dagger для контролю життєвого циклу компонентів
Вінсент Вільямс,

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

4
Ні, але AndroidViewModelробить
4Oh4

1
Але вам потрібно передати екземпляр програми в його конструкторі, це те саме, що отримати доступ до екземпляра програми з нього
Джон

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

3

Ви не повинні використовувати об’єкти, пов’язані з Android, у своєму ViewModel, оскільки мотивом використання ViewModel є розділення Java-коду та Android-коду, щоб ви могли перевірити свою ділову логіку окремо, і ви отримаєте окремий шар компонентів Android та свою бізнес-логіку та даних, Ви не повинні мати контексту у ViewModel, оскільки це може призвести до збоїв


2
Це справедливе спостереження, але для деяких внутрішніх бібліотек все ще потрібні контексти додатків, такі як MediaStore. Відповідь 4gus71n нижче пояснює, як піти на компроміс.
Брайан В. Вагнер,

1
Так, ви можете використовувати контекст програми, але не контекст діяльності, оскільки контекст програми живе протягом усього життєвого циклу програми, але не контекст діяльності, оскільки передача контексту активності до будь-якого асинхронного процесу може призвести до витоків пам'яті. Контекст, згаданий у моєму дописі, - це Activity Контекст. Але все-таки слід подбати про те, щоб не передавати контекст будь-якому асинхронному процесу, навіть якщо це контекст додатків.
Рохіт Шарма

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;

0

Використовуйте такий шаблон:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.