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 AndroidViewModel
ma 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!
android
mvvm
dagger-2
android-context
Vincent Williams
źródło
źródło
AndroidViewModel
ale dostajeCannot create instance exception
, możesz odnieść się do mojej odpowiedzi stackoverflow.com/a/62626408/1055241Odpowiedzi:
Możesz użyć
Application
kontekstu, który jest dostarczany przezAndroidViewModel
, powinieneś rozszerzyć,AndroidViewModel
który jest po prostu a,ViewModel
który zawieraApplication
odniesienie.źródło
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 }
źródło
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.
źródło
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.
źródło
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
źródło
getDrawableRes(@DrawableRes int id)
wewnątrz klasy ResourceProviderTL; 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!
źródło
Jak wspominali inni,
AndroidViewModel
można z niej czerpać, aby pobrać aplikację,Context
ale z tego, co zebrałem w komentarzach, próbujesz manipulować@drawable
s od wewnątrz,ViewModel
co pokonuje cel MVVM.Ogólnie rzecz biorąc, potrzeba posiadania litery „a”
Context
w swoimViewModel
prawie zawsze sugeruje, że powinieneś rozważyć przemyślenie tego, jak podzielić logikę na swoje „View
s” i „ViewModels
.Zamiast
ViewModel
rozwiązywać przedmioty do rysowania i przekazywać je do działania / fragmentu, rozważ, aby fragment / aktywność żonglował przedmiotami do rysowania na podstawie danych posiadanych przezViewModel
. Powiedzmy, że potrzebujesz różnych rysunków, które mają być wyświetlane w widoku w stanie włączonym / wyłączonym - toViewModel
powinno posiadać stan (prawdopodobnie boolowski), aleView
zadaniem 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,
Enum
wartość naR.drawable.*
(np. Kolory kart)A może potrzebujesz
Context
jakiegoś komponentu, którego używasz w swoimViewModel
- następnie utwórz komponent pozaViewModel
i przekaż go. Możesz użyć DI lub singletonów, lub utworzyćContext
komponent zależny bezpośrednio przed zainicjowaniemViewModel
inFragment
/Activity
.Po co zawracać sobie głowę:
Context
to kwestia specyficzna dla Androida, a uzależnienie od nichViewModel
jest 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.źródło
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
ViewModel
tak, jak zwykle, i nadaj mu ApplicationContext za pośrednictwem ViewModelProviders.Factory, jak zwykle.źródło
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.źródło
ViewModel
Klasa nie magetApplication
metody.AndroidViewModel
takNie 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
źródło
Miałem problemy z uzyskaniem
SharedPreferences
podczas korzystania zViewModel
klasy, więc skorzystałem z powyższych porad i wykonałem następujące czynnościAndroidViewModel
. Teraz wszystko wygląda świetnieDla
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; } }
źródło
Stworzyłem to w ten sposób:
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:
źródło
Użyj następującego wzoru:
źródło