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.
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.
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.
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.
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
Author: shijiacheng
Links to this article: http://shijiacheng.studio/2018/07/01/mvx/
Copyright Declaration: Reproduced please indicate the source!