MVC vs. MVP vs. MVVM on Android

Posted by psychosquirrel on Sun, 19 May 2019 11:56:19 +0200

Over the past few years, the way to turn Android applications into logical components has matured. To a large extent, it has moved away from MVC mode to a more modular and testable mode.

Model View Presenter (MVP) & Model View View Model (MVVM) are the two most widely used alternatives. This article does not discuss which approach is more suitable for Android application development, but shows how each pattern is written through a case study.

In this paper, through the implementation of a Tingzi game, through MVC, MVP, MVVM three modes to achieve the game effect. The source code has been uploaded to the Github repository.

Tingzi game effect map. png

MVC

Model, View, Controller divides the application into three responsibilities at the macro level.

Model

Model model is data + state + business logic in application program. It can be said that the brain of an application is not bound by the View view and the Controller controller, so it can be reused in many cases.

View

View view is the presentation of the Model, which is responsible for presenting the UI and communicating with Controller when users interact with the application. In MVC architecture, views are often "silly" because they do not understand the underlying model, nor do they understand the state, or what users do when they interact by clicking buttons, typing values, etc. The idea is that the fewer they know the looser their coupling to the model, the more flexible they will change.

Controller

Controller is the glue that sticks apps together. It is the main controller in the application program. When View tells the Controller user to click the button, Controller decides how to interact with the Model accordingly. Controller can update the status of View as needed, depending on the data changes in Model. In Android applications, Controller is almost always served by Activity or Fragment.

This is the role of each class in our Tingzi game.

mvc model role diagram. png

Let's examine Controller in more detail.

public class MVCTicTacToeActivity extends AppCompatActivity {
    private static String TAG = MVCTicTacToeActivity.class.getName();

    private Board model;

    private ViewGroup buttonGrid;
    private View winnerPlayerViewGroup;
    private TextView winnerPlayerLabel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_mvc_tictactoe);
        winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
        winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
        buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);

        model = new Board();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.menu_tictactoe, menu);
        return true;
    }
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_reset:
                reset();
                return true;
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    public void onCellClicked(View v) {

        Button button = (Button) v;

        String tag = button.getTag().toString();
        int row = Integer.valueOf(tag.substring(0,1));
        int col = Integer.valueOf(tag.substring(1,2));
        Log.i(TAG, "Click Row: [" + row + "," + col + "]");

        Player playerThatMoved = model.mark(row, col);

        if(playerThatMoved != null) {
            button.setText(playerThatMoved.toString());
            if (model.getWinner() != null) {
                winnerPlayerLabel.setText(playerThatMoved.toString());
                winnerPlayerViewGroup.setVisibility(View.VISIBLE);
            }
        }

    }

    private void reset() {
        winnerPlayerViewGroup.setVisibility(View.GONE);
        winnerPlayerLabel.setText("");

        model.restart();

        for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
            ((Button) buttonGrid.getChildAt(i)).setText("");
        }
    }
}

Assessment

MVC does a good job of separating models and views. Of course, the model can be easily tested because it does not depend on anything, and the view has nothing to test at the unit test level. However, the controller has some problems.

Controller Concerns

  • Testability - Controller is closely linked to the Android API and is difficult to unit test.
  • Modularization and flexibility - Controller is tightly coupled with View. It may also be an extension of View. If we change View, we have to go back and change Controller.
  • Maintenance - As time goes on, more and more code begins to move to Controller, making them bulky and fragile.

How can we solve this problem? MVP to save!

MVP

MVP disconnects Controller so that natural View/Activity coupling can occur without being associated with other "Controller" responsibilities. Let's start with MVC comparison again.

Model

Same / unchanged as MVC

View

The only change here is that Activity/Fragment is now considered part of View. Let Activity implement a view interface so that Presnenter has a codeable interface. This eliminates coupling it to any particular page and allows for simple unit testing using the view's mock implementation.

Presenter

This is essentially a Controller for MVC, except that it is totally independent of View and is just an interface. This solves the testability problem and the modularity/flexibility problem we encounter in MVC. In fact, MVP purists argue that Presenter should not have any reference to any Android API or code.

Let's look at the decomposition in MVP in our application.

mvp model role diagram. png

Looking at Presenter in more detail below, the first thing you will notice is how simple and clear the intentions of each action are. It doesn't tell View how to display something, it just tells it what to display.

public class TicTacToePresenter implements Presenter {
    private TicTacToeView view;
    private Board model;

    public TicTacToePresenter(TicTacToeView view){
        this.view = view;
    }

    @Override
    public void onCreate() {
        model = new Board();
    }

    @Override
    public void onPause() {

    }

    @Override
    public void onResume() {

    }

    @Override
    public void onDestroy() {

    }


    public void onButtonSelected(int row, int col) {
        Player playerThatMoved = model.mark(row, col);

        if(playerThatMoved != null) {
            view.setButtonText(row, col, playerThatMoved.toString());

            if (model.getWinner() != null) {
                view.showWinner(playerThatMoved.toString());
            }
        }
    }

    public void onResetSelected() {
        view.clearWinnerDisplay();
        view.clearButtons();
        model.restart();
    }
}

In order to do this without binding Activity to Presenter, we created an interface implemented by Activity. In testing, we will create a mock based on this interface to test interaction with Presenter and View.

public interface TicTacToeView {
    void showWinner(String winnerLabel);
    void clearWinnerDisplay();
    void clearButtons();
    void setButtonText(int row,int col,String text);
}

Assessment

We can easily unit test Presenter logic because it is not bound to any Android-specific View and API, and we can use any other View as long as View implements the * TicTacToeView * interface.

Presenter's attention

  • Maintenance - Presenter, like Controller, tends to collect additional business logic over time. At some point, developers often find themselves with a cumbersome Presenter that is hard to separate.

Of course, developers can be careful to prevent this from happening. However, MVVM can better solve this problem.

MVVM

On Android Have Data binding MVVM has the advantages of easy testing and modularization, and it also reduces the number of code that we have to write to connect View +Model.

Let's look at the various parts of MVVM.

Model

Same / unchanged as MVC

View

The View is bound to the viewModel in a flexible way to monitor observable variables and operations.

ViewModel

ViewModel is responsible for wrapping the Model and preparing the observable data needed by View. It also provides View with hocks to pass events to Model. However, ViewModel does not depend on View.

Our program is decomposed in early MVVM mode.

mvvm model role diagram. png

Let's take a closer look at the code here, starting with ViewModel.

public class TicTacToeViewModel implements ViewModel {
    private Board model;
    public final ObservableArrayMap<String,String> cells = new ObservableArrayMap<>();
    public final ObservableField<String> winner = new ObservableField<>();

    public TicTacToeViewModel(){
        model = new Board();
    }

    @Override
    public void onCreate() {

    }

    @Override
    public void onPause() {

    }

    @Override
    public void onResume() {

    }

    @Override
    public void onDestroy() {

    }

    public void onResetClick(){
        model.restart();
        winner.set(null);
        cells.clear();
    }

    public void onCellClick(int row, int col){
        Player player = model.mark(row,col);
        cells.put(""+row+col,player == null?null:player.toString());
        winner.set(model.getWinner()==null?null:model.getWinner().toString());
    }
}

Look at the xml file to see how these variables and events are bound.

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>
        <import type="android.view.View" />
        <variable
            name="player"
            type="com.shijc.mvx.mvvm.viewmodel.TicTacToeViewModel"/>
    </data>

    <LinearLayout
        android:id="@+id/tictactoe"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center_horizontal"
        android:orientation="vertical"
        tools:context="com.acme.tictactoe.view.TicTacToeActivity">

        <GridLayout
            android:id="@+id/buttonGrid"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:columnCount="3"
            android:rowCount="3">

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(0,0)}"
                android:text='@{player.cells["00"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(0,1)}"
                android:text='@{player.cells["01"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(0,2)}"
                android:text='@{player.cells["02"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(1,0)}"
                android:text='@{player.cells["10"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(1,1)}"
                android:text='@{player.cells["11"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(1,2)}"
                android:text='@{player.cells["12"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(2,0)}"
                android:text='@{player.cells["20"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(2,1)}"
                android:text='@{player.cells["21"]}' />

            <Button
                style="@style/tictactoebutton"
                android:onClick="@{() -> player.onCellClick(2,2)}"
                android:text='@{player.cells["22"]}' />

        </GridLayout>


        <LinearLayout
            android:id="@+id/winnerPlayerViewGroup"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:gravity="center"
            android:orientation="vertical"
            android:visibility="@{player.winner == null ? View.GONE:View.VISIBLE}"
            tools:visibility="visible">

            <TextView
                android:id="@+id/winnerPlayerLabel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_margin="20dp"
                android:textSize="40sp"
                android:text="@{player.winner}"
                tools:text="X" />

            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Winner"
                android:textSize="30sp" />

        </LinearLayout>

    </LinearLayout>

</layout>

Assessment

Unit testing is easier now because you really don't depend on View. When testing, you just need to verify that the observable variables are set correctly when the Model changes. There is no need for mock to test View, because there is MVP mode.

MVVM concerns

  • Maintenance - Since View can be bound to variables and expressions, irrelevant presentation logic may change over time, effectively adding code to our XML. To avoid this situation, you always get values directly from the ViewModel, rather than trying to compute or derive them in view binding expressions. In this way, unit testing can be carried out properly.

conclusion

MVP and MVVM do better than MVC in decomposing applications into modular single-purpose components, but they also increase the complexity of applications. For very simple applications with only one or two screens, MVC may work well. MVVM with data binding is attractive because it follows a more reactive programming model and generates less code.

If you are interested in seeing more examples of MVP and MVVM in practice, I encourage you to look at them. Google Architecture Blueprints Project. There are also many blog articles exploring these models in depth.

Reference resources

https://academy.realm.io/posts/eric-maxwell-mvc-mvp-and-mvvm-on-android

Source address

Author: shijiacheng
Links to this article: http://shijiacheng.studio/2018/07/01/mvx/
Copyright Declaration: Reproduced please indicate the source!

Topics: Android Fragment xml github