Jak uzyskać kontekst w modelu widoku Android MVVM


90

Próbuję zaimplementować wzorzec MVVM w mojej aplikacji na Androida. Czytałem, że ViewModels nie powinny zawierać kodu specyficznego dla Androida (aby ułatwić testowanie), jednak muszę używać kontekstu do różnych rzeczy (pobieranie zasobów z xml, inicjowanie preferencji itp.). Jaki jest najlepszy sposób, aby to zrobić? Widziałem, że AndroidViewModelma odniesienie do kontekstu aplikacji, jednak zawiera on kod specyficzny dla Androida, więc nie jestem pewien, czy powinien on znajdować się w ViewModel. Te również wiążą się ze zdarzeniami cyklu życia działania, ale używam sztyletu do zarządzania zakresem komponentów, więc nie jestem pewien, jak to wpłynie na to. Jestem nowy w MVVM i Dagger, więc każda pomoc jest mile widziana!


Na wszelki wypadek, gdy ktoś próbuje użyć, AndroidViewModelale dostaje Cannot create instance exception, możesz odnieść się do mojej odpowiedzi stackoverflow.com/a/62626408/1055241
gprathour

Nie powinieneś używać Context w ViewModel, zamiast tego utwórz UseCase, aby uzyskać kontekst w ten sposób
Ruben Caster

Odpowiedzi:


71

Możesz użyć Applicationkontekstu, który jest dostarczany przez AndroidViewModel, powinieneś rozszerzyć, AndroidViewModelktóry jest po prostu a, ViewModelktóry zawiera Applicationodniesienie.


Działał jak urok!
SPM

Czy ktoś mógłby pokazać to w kodzie? Jestem na Jawie
Biswas Khayargoli

55

Model widoku komponentów architektury systemu Android,

Przekazywanie kontekstu działania do ViewModel działania nie jest dobrą praktyką, ponieważ jest to wyciek pamięci.

W związku z tym, aby uzyskać kontekst w Twoim ViewModel, klasa ViewModel powinna rozszerzać klasę modelu widoku systemu Android . W ten sposób możesz uzyskać kontekst, jak pokazano w przykładowym kodzie poniżej.

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

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

    //... ViewModel methods 

}

2
Dlaczego nie użyć bezpośrednio parametru aplikacji i normalnego ViewModel? Nie widzę sensu w „getApplication <Application> ()”. Po prostu dodaje schematu.
Niesamowity

50

Nie chodzi o to, że ViewModels nie powinny zawierać kodu specyficznego dla Androida, aby ułatwić testowanie, ponieważ jest to abstrakcja, która ułatwia testowanie.

Powodem, dla którego ViewModels nie powinny zawierać wystąpienia Context ani niczego takiego jak widoki lub inne obiekty, które przechowują Context, jest to, że ma oddzielny cykl życia niż Działania i fragmenty.

Mam na myśli to, że powiedzmy, że dokonujesz zmiany rotacji w swojej aplikacji. Powoduje to, że Twoja Aktywność i Fragment niszczą się, więc odtwarzają się. ViewModel ma trwać w tym stanie, więc istnieje ryzyko awarii i innych wyjątków, jeśli nadal zawiera widok lub kontekst do zniszczonego działania.

Jeśli chodzi o to, jak powinieneś robić to, co chcesz, MVVM i ViewModel działają naprawdę dobrze z komponentem Databinding w JetPack. W przypadku większości rzeczy, dla których zwykle przechowujesz String, int lub itp., Możesz użyć Databinding, aby widoki wyświetlały je bezpośrednio, bez konieczności przechowywania wartości w ViewModel.

Ale jeśli nie chcesz powiązania danych, nadal możesz przekazać Context wewnątrz konstruktora lub metod, aby uzyskać dostęp do zasobów. Po prostu nie trzymaj wystąpienia tego kontekstu w swoim ViewModel.


1
Zrozumiałem, że włączenie kodu specyficznego dla Androida wymaga uruchomienia testów instrumentacji, co jest znacznie wolniejsze niż zwykłe testy JUnit. Obecnie używam Databinding dla metod klikania, ale nie widzę, jak pomogłoby to w pobieraniu zasobów z xml lub w preferencjach. Właśnie zdałem sobie sprawę, że dla preferencji potrzebowałbym również kontekstu wewnątrz mojego modelu. To, co obecnie robię, to wstrzyknięcie przez Dagger kontekstu aplikacji (moduł kontekstu pobiera go z metody statycznej wewnątrz klasy aplikacji)
Vincent Williams

@VincentWilliams Tak, użycie ViewModel pomaga oddzielić kod od składników interfejsu użytkownika, co ułatwia przeprowadzanie testów. Ale mówię, że głównym powodem nieuwzględniania kontekstu, widoków itp. Nie są przyczyny testowania, ale cykl życia ViewModel, który może pomóc w uniknięciu awarii i innych błędów. Jeśli chodzi o wiązanie danych, może to pomóc w przypadku zasobów, ponieważ większość czasu potrzebnego do uzyskania dostępu do zasobów w kodzie wynika z konieczności zastosowania tego ciągu, koloru i wymiaru w układzie, co można wykonać bezpośrednio w przypadku wiązania danych.
Jackey,

OK, rozumiem, co masz na myśli, ale wiązanie danych nie pomoże mi w tym przypadku, ponieważ potrzebuję dostępu do ciągów znaków do użycia w modelu (można je umieścić w klasie stałych zamiast XML, jak przypuszczam), a także do inicjalizacji SharedPreferences
Vincent Williams

3
jeśli chcę przełączać tekst w widoku tekstowym na podstawie viewmodelu formularza wartości, ciąg musi być zlokalizowany, więc potrzebuję zasobów w moim modelu widoku, bez kontekstu, w jaki sposób mogę uzyskać dostęp do zasobów?
Srishti Roy

3
@SrishtiRoy Jeśli używasz wiązania danych, możesz łatwo przełączać tekst TextView na podstawie wartości z modelu widoku. Nie ma potrzeby dostępu do kontekstu wewnątrz Twojego ViewModel, ponieważ wszystko to dzieje się w plikach układu. Jeśli jednak musisz użyć Context w swoim ViewModel, powinieneś rozważyć użycie AndroidViewModel zamiast ViewModel. AndroidViewModel zawiera kontekst aplikacji, który można wywołać za pomocą metody getApplication (), dzięki czemu powinien spełniać wymagania kontekstu, jeśli ViewModel wymaga kontekstu.
Jackey,

15

Krótka odpowiedź - nie rób tego

Czemu ?

Zaprzecza całemu celowi modeli widoku

Prawie wszystko, co możesz zrobić w modelu widoku, można wykonać w działaniu / fragmencie, używając instancji LiveData i różnych innych zalecanych metod.


21
Dlaczego więc klasa AndroidViewModel w ogóle istnieje?
Alex Berdnikov

1
@AlexBerdnikov Celem MVVM jest odizolowanie widoku (działania / fragmentu) z ViewModel nawet bardziej niż MVP. Aby łatwiej było przetestować.
hushed_voice

3
@free_style Dzięki za wyjaśnienie, ale pytanie nadal pozostaje aktualne: jeśli nie możemy zachowywać kontekstu w ViewModel, to dlaczego klasa AndroidViewModel w ogóle istnieje? Jego celem jest zapewnienie kontekstu aplikacji, prawda?
Alex Berdnikov

6
@AlexBerdnikov Używanie kontekstu działania w modelu widoku może powodować wycieki pamięci. Tak więc, używając klasy AndroidViewModel, zostaniesz dostarczony przez kontekst aplikacji, który (miejmy nadzieję) nie spowoduje przecieku pamięci. Dlatego użycie AndroidViewModel może być lepsze niż przekazywanie do niego kontekstu działania. Jednak nadal będzie to utrudniać testowanie. To jest moje podejście do tego.
hushed_voice

1
Nie mogę uzyskać dostępu do pliku z folderu res / raw z repozytorium?
Fugogugo

14

To, co ostatecznie zrobiłem zamiast mieć Context bezpośrednio w ViewModel, utworzyłem klasy dostawców, takie jak ResourceProvider, które dałyby mi potrzebne zasoby, i miałem te klasy dostawców wstrzyknięte do mojego ViewModel


1
Używam ResourcesProvider z sztyletem w AppModule. Czy to dobre podejście do pobierania kontekstu z ResourcesProvider lub AndroidViewModel jest lepsze, aby uzyskać kontekst dla zasobów?
Usman Rana

@Vincent: Jak korzystać z resourceProvider, aby uzyskać możliwość rysowania wewnątrz ViewModel?
HoangVu

@Vegeta Dodałbyś metodę taką jak getDrawableRes(@DrawableRes int id)wewnątrz klasy ResourceProvider
Vincent Williams

1
Jest to sprzeczne z podejściem Clean Architecture, które stwierdza, że ​​zależności frameworków nie powinny przekraczać granic w logice domeny (ViewModels).
Igor Ganapolsky

1
Maszyny wirtualne @IgorGanapolsky nie są dokładnie logiką domeny. Logika domeny to inne klasy, takie jak interaktory i repozytoria, aby wymienić tylko kilka. Maszyny wirtualne należą do kategorii „klej”, ponieważ wchodzą w interakcje z Twoją domeną, ale nie bezpośrednio. Jeśli Twoje maszyny wirtualne są częścią Twojej domeny, powinieneś ponownie rozważyć, w jaki sposób używasz wzorca, ponieważ dajesz im zbyt dużą odpowiedzialność.
mradzinski

8

TL; DR: Wstrzyknij kontekst Aplikacji za pomocą Daggera w swoich ViewModels i użyj go do załadowania zasobów. Jeśli musisz załadować obrazy, przekaż wystąpienie View za pomocą argumentów z metod wiązania danych i użyj tego kontekstu widoku.

MVVM to dobra architektura i zdecydowanie jest to przyszłość rozwoju Androida, ale jest kilka rzeczy, które wciąż są ekologiczne. Weźmy na przykład komunikację warstwową w architekturze MVVM. Widziałem, jak różni programiści (bardzo znani) używają LiveData do komunikowania się z różnymi warstwami na różne sposoby. Niektórzy z nich używają LiveData do komunikacji ViewModel z UI, ale potem używają interfejsów wywołań zwrotnych do komunikacji z Repozytoriami lub mają Interactors / UseCases i używają LiveData do komunikacji z nimi. Chodzi o to, że nie wszystko jest jeszcze zdefiniowane w 100% .

Biorąc to pod uwagę, moim podejściem do twojego konkretnego problemu jest posiadanie kontekstu aplikacji dostępnego przez DI do użycia w moich ViewModels, aby uzyskać takie rzeczy jak String z mojego strings.xml

Jeśli mam do czynienia z ładowaniem obrazu, próbuję przejść przez obiekty View z metod adaptera Databinding i użyć kontekstu widoku, aby załadować obrazy. Czemu? ponieważ niektóre technologie (na przykład Glide) mogą napotkać problemy, jeśli używasz kontekstu aplikacji do ładowania obrazów.

Mam nadzieję, że to pomoże!


5
TL; DR powinno być na szczycie
Jacques Koorts

1
Dziękuję za Twoją odpowiedź. Jednak dlaczego miałbyś używać sztyletu do wstrzyknięcia kontekstu, jeśli mógłbyś rozszerzyć swój model widoku z androidviewmodel i użyć wbudowanego kontekstu, który zapewnia sama klasa? Zwłaszcza biorąc pod uwagę śmieszną ilość gotowego kodu, który sprawia, że ​​sztylet i MVVM współpracują ze sobą, inne rozwiązanie wydaje się znacznie jaśniejsze imo. Co o tym myślisz?
Josip Domazet

7

Jak wspominali inni, AndroidViewModelmożna z niej czerpać, aby pobrać aplikację, Contextale z tego, co zebrałem w komentarzach, próbujesz manipulować @drawables od wewnątrz, ViewModelco pokonuje cel MVVM.

Ogólnie rzecz biorąc, potrzeba posiadania litery „a” Contextw swoim ViewModelprawie zawsze sugeruje, że powinieneś rozważyć przemyślenie tego, jak podzielić logikę na swoje „ Views” i „ ViewModels.

Zamiast ViewModelrozwiązywać przedmioty do rysowania i przekazywać je do działania / fragmentu, rozważ, aby fragment / aktywność żonglował przedmiotami do rysowania na podstawie danych posiadanych przez ViewModel. Powiedzmy, że potrzebujesz różnych rysunków, które mają być wyświetlane w widoku w stanie włączonym / wyłączonym - to ViewModelpowinno posiadać stan (prawdopodobnie boolowski), ale Viewzadaniem firmy jest wybranie odpowiedniego elementu do rysowania.

Z DataBinding można to zrobić całkiem łatwo :

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

Jeśli masz więcej stanów i rysunków, aby uniknąć nieporęcznej logiki w pliku układu, możesz napisać niestandardowy BindingAdapter, który tłumaczy, powiedzmy, Enumwartość na R.drawable.*(np. Kolory kart)

A może potrzebujesz Contextjakiegoś komponentu, którego używasz w swoim ViewModel- następnie utwórz komponent poza ViewModeli przekaż go. Możesz użyć DI lub singletonów, lub utworzyć Contextkomponent zależny bezpośrednio przed zainicjowaniem ViewModelin Fragment/ Activity.

Po co zawracać sobie głowę: Contextto kwestia specyficzna dla Androida, a uzależnienie od nich ViewModeljest złą praktyką: przeszkadzają w testowaniu jednostkowym. Z drugiej strony, Twoje własne interfejsy komponentów / usług są w pełni pod Twoją kontrolą, dzięki czemu możesz łatwo mockować je do testów.


5

zawiera odniesienie do kontekstu aplikacji, który zawiera jednak kod specyficzny dla systemu Android

Dobra wiadomość, możesz użyć Mockito.mock(Context.class)i sprawić, by kontekst zwrócił cokolwiek chcesz w testach!

Więc po prostu użyj ViewModeltak, jak zwykle, i nadaj mu ApplicationContext za pośrednictwem ViewModelProviders.Factory, jak zwykle.


3

można uzyskać dostęp do kontekstu aplikacji getApplication().getApplicationContext()z poziomu ViewModel. To jest to, czego potrzebujesz, aby uzyskać dostęp do zasobów, preferencji itp.


Chyba zawężę moje pytanie. Czy to źle mieć odwołanie do kontekstu wewnątrz modelu widoku (czy nie ma to wpływu na testowanie?) I czy użycie klasy AndroidViewModel wpłynęłoby w jakikolwiek sposób na Dagger? Czy nie jest to związane z cyklem życia działalności? Używam sztyletu do kontrolowania cyklu życia komponentów
Vincent Williams,

14
ViewModelKlasa nie ma getApplicationmetody.
beroal

4
Nie, ale AndroidViewModeltak
4Oh4

1
Ale musisz przekazać instancję Application w jej konstruktorze, to jest to samo, co uzyskanie z niej dostępu do instancji aplikacji
John Sardinha

2
Kontekst aplikacji nie stanowi dużego problemu. Nie chcesz mieć kontekstu działania / fragmentu, ponieważ jesteś zepsuty, jeśli fragment / aktywność zostanie zniszczona, a model widoku nadal ma odniesienie do teraz nieistniejącego kontekstu. Ale nigdy nie zostanie zniszczony kontekst APLIKACJI, ale maszyna wirtualna nadal ma do niego odniesienie. Dobrze? Czy możesz sobie wyobrazić scenariusz, w którym Twoja aplikacja zostanie zamknięta, ale Viewmodel nie? :)
user1713450

3

Nie należy używać obiektów związanych z systemem Android w swoim ViewModel, ponieważ motywem korzystania z ViewModel jest oddzielenie kodu java i kodu Androida, aby można było osobno przetestować logikę biznesową i mieć oddzielną warstwę składników Androida i logiki biznesowej i danych, nie powinieneś mieć kontekstu w swoim ViewModel, ponieważ może to prowadzić do awarii


2
To uczciwa obserwacja, ale niektóre z bibliotek zaplecza nadal wymagają kontekstów aplikacji, takich jak MediaStore. Odpowiedź 4gus71n poniżej wyjaśnia, jak iść na kompromis.
Bryan W. Wagner

1
Tak, możesz używać kontekstu aplikacji, ale nie kontekstu działań, ponieważ kontekst aplikacji żyje przez cały cykl życia aplikacji, ale nie kontekst działania, ponieważ przekazanie kontekstu działania do dowolnego procesu asynchronicznego może spowodować wycieki pamięci. Kontekst, ale nadal należy uważać, aby nie przekazywać kontekstu do żadnego procesu asynchronicznego, nawet jeśli jest to kontekst aplikacji.
Rohit Sharma

2

Miałem problemy z uzyskaniem SharedPreferencespodczas korzystania z ViewModelklasy, więc skorzystałem z powyższych porad i wykonałem następujące czynności AndroidViewModel. Teraz wszystko wygląda świetnie

Dla 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>"));
    }

}

A w 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

Stworzyłem to w ten sposób:

@Module
public class ContextModule {

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

A potem właśnie dodałem w AppComponent plik ContextModule.class:

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

Następnie wstawiłem kontekst do mojego ViewModel:

@Inject
@Named("AppContext")
Context context;

0

Użyj następującego wzoru:

class NameViewModel(
val variable:Class,application: Application):AndroidViewModel(application){
   body...
}
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.