Zastosowanie
MVVM
(Model-View-ViewModel
) ma za zadanie ułatwić tworzenie ekranów aplikacji poprzez zastosowanie podziału odpowiedzialności na trzy różne warstwy: widoku (View
), widoku modelu (ViewModel
) oraz modelu (Model
). Warstwa widoku
odpowiedzialna jest za prezentacje danych, stanu systemu i bieżących operacji w interfejsie graficznym, a także za inicjalizację i wiązanie ViewModel
z elementami widoku. Warstwa widoku modelu
zajmuje się dostarczaniem danych modelu dla warstwy widoku oraz podejmowaniem akcji na rzecz wywołanego zdarzenia z widoku. Natomiast warstwa modelu
odpowiada za logikę biznesową, czyli przetwarzanie, przechowywanie, modyfikacje oraz dostarczanie oczekiwanych danych do widoku modelu. Dzięki zastosowaniu strategii wiązania danych (data binding
) w warstwie widoku minimalizowana jest jego logika, kod staje się bardziej uporządkowany i otwarty na modyfikacje, a przeprowadzenie testów łatwiejsze. Idea wzorca MVVM
opiera się przede wszystkim na obserwowaniu przez warstwę widoku (wzorzec Obserwator
) zmieniających się danych w warstwie widoku modelu i reagowanie na zmiany poprzez mechanizm wiązania danych. Ze względu na różnorodność środowisk i technologii realizacja wzorca MVVM
może zostać uzyskana na wiele sposobów.
Ograniczenia
Każdy ekran widoku posiada dedykowany widok modelu co przekłada się na większą ilość klas (w porównaniu do wzorca MVP
nadal jest ich mniej). Ponadto warstwa widoku musi zadbać o właściwe wiązanie zmiennych i metod dla każdego wymaganego elementu widoku, a także o obserwowanie stanu widoku modelu. Ze względu na wybór dostępnych technik, bibliotek i komponentów służących realizacji wiązania danych oraz obserwowania ich stanu implementacja wzorca MVVM
nie należy do łatwych. Co więcej mnogość strategii implementacji MVVM
sprawia, że dane rozwiązanie może być niezrozumiałe dla innych programistów. Stworzenie poprawnego i kompletnego widoku modelu wymaga przeprowadzenia analizy warstwy widoku pod kątem wymaganych danych i możliwych stanów. Zawarty kod logiki kontrolek widoku w plikach layout
może być nieczytelny i trudny do testowania, dlatego warto rozważyc umieszczanie pewnych fragmentów logiki w klasie widoku.
Użycie
Wzorzec MVVM
jest odpowiedzią na problem God Acticitiy
, czyli Aktywności
o dużej odpowiedzialności i dużej ilości kodu. Ponadto rozwiązuje on problem wycieku pamięci pojawiający się we wzorcu MVP
(widok modelu nie posiada referencji do warstwy widoku). Dzięki separacji zadań w różnych warstwach wzorzec może być używany, aby zwiększyć czytelność kodu i możliwość niezależnego testowania bez użycia dodatkowych zależności. Mechanizm wiązania danych zwiększa elastyczność widoku na modyfikacje co sprawia, że wzorzec MVVM
sprawdzi się dobrze w dużych dynamicznie zmieniających się projektach.
Implementacja
Warstwa widoku View
tworzy instancje widoku modelu ViewModel
oraz wiąże go z widokiem za pomocą mechanizmu data binding
. Klasa ViewModel
przechowuje wszystkie niezbędne dane dla warstwy widoku, które otrzymuje z modelu. Dzięki zastosowaniu wzorca Obserwator
na polach widoku modelu każda zmiana stanu jest odnotowana przez widok. Warstwa modelu Model
dostarcza implementacje pobierania, modyfikacji i przetwarzania danych z repozytoriów.
Poniższy listing przedstawia realizację wzorca MVVM
z pominięciem zależności Android
oraz implementacji wzorca Obserwator
.
public class View {
//this class initialize everything so in Android it could be Activity
//for simplify MVVM idea any Android class is used
//some view fields like buttons, textview etc
private TextView textView;
private EditText editText;
private Button button;
private ViewModel viewModel;
public View() {
initViewModel();
initView();
}
private void initViewModel() {
this.viewModel = new ViewModel();
//observe viewModel state and prepare on changed listeners
}
private void initView() {
//init views and set default text/images
//this can be done in layout by data binding
}
}
<layout>
<!-- some parent layout -->
<!-- import some classes if needed -->
<!-- bind ViewModel instance from View class as viewModel variable -->
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.input}"/> <!-- two way data binding -->
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Check"
android:onClick="@{viewModel.onButtonClicked}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="@{viewModel.data.equals("EMPTY") ? View.GONE : View.VISIBLE}"
android:text="@{viewModel.data}"/>
</layout>
public class ViewModel {
//those fields should be observable
private String data = "";
private String input = "";
private Model model;
public ViewModel() {
model = new Model();
}
private void onButtonClicked() {
//do proper action
try {
data = model.getStatus(input);
}
catch(Exception exception) {
data = "No data has found";
}
}
}
public class Model {
private Database database;
//other variables, logic managers or data providers like database, cache, network
public Model() {
//do init staff
//initialize some instances
database = new Database();
}
public String getStatus(String input) {
String result = database.get("STATUS", input);
if(result.equals(""))
return "EMPTY";
else
return result;
}
}
Implementacja wzorca MVVM
może zostać zrealizowana na wiele różnych sposobów i wariantów. Warstwa widoku View
może wykorzystywać mechanizm data binding
w plikach layout
lub w kodzie klasy widoku. Jest również możliwe uzyskanie implementacji wzorca bez użycia wiązania danych, a wszelkie zmiany odnotowane są poprzez śledzenie zmian w ViewModel
z wykorzystaniem wzorca Obserwator
. Istnieje także wiele podejść do informowania warstwy widoku o wystąpieniu błędu lub postępu operacji. Jedną ze strategii jest traktowanie takich zdarzeń jako pola w klasie widoku modelu i nasłuchiwanie przez widok zmiany stanu i podejmowaniu właściwej akcji. Komunikacja między warstwą modelu Model
, a widoku modelu ViewModel
może przebiegać przy wykorzystaniu natywnych rozwiązań, funkcji zwrotu (callback
), mechanizmu Interactor
(znanego z MVP
), szyny zdarzeń (event bus
), wzorca Obserwator
czy też użycia biblioteki do programowania reaktywnego jak np. RxJava
. Niezależnie od wyboru strategii implementacji należy przede wszystkim pamiętać, aby widok modelu nie posiadał zależności Android
oraz referencji do Aktywności
, a warstwa widoku obserwowała stan ViewModel
.
Przykład
Aplikacja Scorer ułatwia rejestrowanie i śledzenie zdarzeń z rozgrywek meczu piłkarskiego (zdobyte bramki, żółte i czerwone kartki, kontuzje itp). Obsługujący ją sędzia techniczny wybiera typ wydarzenia oraz wskazuje zawodnika. Dodane wydarzenie automatycznie pojawia się w interfejsie graficznym. Do realizacji tego zadania wykorzystano wzorzec MVVM
w wariancie z użyciem komponentów architektury Android
.
public class MatchActivity extends AppCompatActivity {
private MatchViewModel viewModel;
private ActivityMatchBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
viewModel = ViewModelProviders.of(this).get(MatchViewModel.class); //auto restore
initBinding();
initObservers();
}
private void initBinding() {
binding = DataBindingUtil.setContentView(this, R.layout.activity_match);
binding.setViewModel(viewModel);
binding.setLifecycleOwner(this);
}
private void initObservers() {
viewModel.getIsProgress().observe(this, aBoolean -> {
//show or hide progress
});
viewModel.getState().observe(this, state -> {
//show some message based on status code or do something else
});
}
}
<layout>
<data>
<import type="java.lang.Integer" />
<variable name="viewModel" type="pl.androidcode.mvvm.MatchViewModel"/>
</data>
<!-- some parent layout and other views -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{Integer.toString(viewModel.teamAScore)}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{Integer.toString(viewModel.teamBScore)}"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@{viewModel.events}"/>
<EditText
android:id="@+id/editText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={viewModel.player}"/> <!-- two way data binding -->
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Team A Goal"
android:onClick="@{viewModel::onButtonAScoreClicked}"/>
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Team B Goal"
android:onClick="@{viewModel::onButtonBScoreClicked}"/>
<!-- some other views and viewgroups -->
</layout>
public class MatchViewModel extends ViewModel {
private MutableLiveData<Integer> teamAScore = new MutableLiveData<>();
private MutableLiveData<Integer> teamBScore = new MutableLiveData<>();
private MutableLiveData<String> events = new MutableLiveData<>();
private MutableLiveData<String> player = new MutableLiveData<>();
private MutableLiveData<Boolean> isProgress = new MutableLiveData<>();
private MutableLiveData<State> state = new MutableLiveData<>();
private MatchModel model;
public MyViewModel() {
teamAScore.setValue(0);
teamBScore.setValue(0);
events.setValue("");
player.setValue("");
isProgress.setValue(false);
state.setValue(NONE);
model = new MatchModel();
}
public LiveData<Integer> getTeamAScore() {
return teamAScore;
}
public LiveData<Integer> getTeamBScore() {
return teamBScore;
}
//other get methods for all LiveData fields
public void onButtonScoreAClicked(View view) {
if(player.getValue.equals("")) {
state.setValue(EMPTY_PLAYER);
}
else {
isProgress.setValue(true);
teamAScore.setValue(teamScoreA.getValue() + 1);
model.goal(player.getValue());
events.setValue(model.getEvents()); //could be some model callback
player.setValue("");
isProgress.setValue(false);
state.setValue(EVENT_ADDED);
}
}
public void onButtonScoreBClicked(View view) {
//do the same way as onButtonsScoreAClicked but for Team B
}
//other handler events method like on yellow, red card or injury
}
public class MatchModel {
private Database database;
//other variables, logic managers or data providers like database, cache, network
public MatchModel() {
//do init staff
//initialize some instances
database = new Database();
}
public void goal(String player) {
database.add("GOAL", player);
}
//other methods like yellow, red card or injury
public String getEvents() {
return database.getAllEvents(); //some string summary from all event
}
}
Biblioteki
Implementacje wzorca MVVM
w Androidzie
może zostać zrealizowana m.in. za pomocą następujących komponentów architektury: DataBinding
, ViewModel
oraz LiveData
. Ponadto w implementacji nierzadko wykorzystywane są biblioteki RxJava
oraz Dagger2
.