MVVM

Wzorce architektoniczne

  |   11 min czytania

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.

MVVM diagram

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.