原文地址:https://realm.io/news/eric-maxwell-mvc-mvp-and-mvvm-on-android/
在过去的几年中,将Android应用程序组织的成逻辑性组件的实践得到了不断进步。整个结构已经从巨大的MVC模式转向了更加模块化,可测试的模式。
MVP和MVVM是最广泛使用的两种替代方式,但是开发者经常讨论它们那个更适合Android。在过去一年中有大量的博文强烈的支持一种比另一个好,但是通常最后都演变为对客观标准的争吵。与其争论哪个方案更好,本文从客观的角度审视所有三种方案的价值及潜在的问题,所以你可以自己做决定。
为了帮助我们了解每个模式的行为,我们需要一个简单的拼字游戏。
本文余下部分将按顺序一步步讲解MVC,MVP和MVVM模式。在开始每个部分前我们先来看看主要组件常见定义和职责,然后看看它们在我们游戏中的应用。
例子的源代码在Github库上可以找到。想要浏览代码,检出你所阅读部分的分支即可。
MVC
模型层、视图层、控制层方法将你应用程序的职责在宏观级别分成3部分。
Model
模型层是我们拼图游戏的数据+状态+业务逻辑部分。所以说它是我们应用程序的大脑。它不和视图层或控制层捆绑,正因如此它在很多环境下可以重用。
View
视图层是模型层的代表。视图层的职责是提供用户交互,当用户与应用程序交互时与控制层交流。在MVC架构中,视图层通常很“蠢”,因为它们不了解基本的模型层也不懂状态,抑或是当用户点击按钮,输入等交互时应该做什么。它们知道的越少,它们和模型层的关联越少,因此它们改变起来更加灵活。
Controller
控制层是胶,将应用程序粘在一起。它是应用程序的主要控制者。当视图层告诉控制层用户点击了一个按钮时,控制层决定如何和模型层交互。基于模型层的数据交换,控制层可以适时地决定更新视图层的状态。在Android应用程序中,控制层大多数都是由Activity或Fragment担任。
Here’s what that looks like at a high level in our Tic Tac Toe app and the classes that play each part.
让我们更详细地检查控制层。
public class TicTacToeActivity extends AppCompatActivity {
private Board model;
/* View Components referenced by the controller */
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
/**
* In onCreate of the Activity we lookup & retain references to view components
* and instantiate the model.
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.tictactoe);
winnerPlayerLabel = (TextView) findViewById(R.id.winnerPlayerLabel);
winnerPlayerViewGroup = findViewById(R.id.winnerPlayerViewGroup);
buttonGrid = (ViewGroup) findViewById(R.id.buttonGrid);
model = new Board();
}
/**
* Here we inflate and attach our reset button in the menu.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_tictactoe, menu);
return true;
}
/**
* We tie the reset() action to the reset tap event.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_reset:
reset();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* When the view tells us a cell is clicked in the tic tac toe board,
* this method will fire. We update the model and then interrogate it's state
* to decide how to proceed. If X or O won with this move, update the view
* to display this and otherwise mark the cell that was clicked.
*/
public void onCellClicked(View v) {
Button button = (Button) v;
int row = Integer.valueOf(tag.substring(0,1));
int col = Integer.valueOf(tag.substring(1,2));
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);
}
}
}
/**
* On reset, we clear the winner label and hide it, then clear out each button.
* We also tell the model to reset (restart) it's state.
*/
private void reset() {
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
model.restart();
for( int i = 0; i < buttonGrid.getChildCount(); i++ ) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
}
进化
MVC在分离模型层和视图层上很不错。无疑地模型层可以很容易地被测试,因为它不和其他层捆绑,视图层在单元测试中没有什么可以测试的。然而控制层则有一些问题了。
控制层的问题
可测试性—控制层和Android的API相捆绑,因此很难进行单元测试。
模块化和灵活性—控制层和视图层关联十分紧密。它可以是视图层的扩展。如果我们改变了视图层,我们也必须回过头来修改控制层。
维护—随着时间的推移,特别是在缺乏模型层的应用程序中,越来越多的代码被移到了控制层中,让它们变的臃肿和脆弱。
我们怎么对待这些呢?用MVP来拯救!
MVP
MVP打散了控制层,所以自然地视图/活动的发生可以不用和剩下的控制层职责相关联。接下来,让我们再一次像MVC那样来定义每层的职责。
模型层
和MVC一样,没有变化。
视图层
这里唯一变化的是Activity/Fragment被认为是视图层的一部分。我们停止了与它们携手同行的自然趋势。好的方式是让Activity实现视图接口,因此表现层有接口可以编码。这样消除了它和特定视图层的耦合,让视图层通过模拟实现来进行简单的单元测试。
表现层
除了和视图层无关,仅是接口外,这本质上是MVC中的控制层。这不但解决了我们在MVC中可测试性的担忧,也解决了模块化/灵活性的问题。实际上,MVP纯粹主义者会主张表现层中不引用任何Android的API或代码。
让我们再来看看我们的应用程序是怎样的。
看看表现层的细节时,你首先会意识到每个行为都是如此简单明了。相比告诉视图层如何显示什么,它只告诉它应该显示什么。
public class TicTacToePresenter implements Presenter {
private TicTacToeView view;
private Board model;
public TicTacToePresenter(TicTacToeView view) {
this.view = view;
this.model = new Board();
}
// Here we implement delegate methods for the standard Android Activity Lifecycle.
// These methods are defined in the Presenter interface that we are implementing.
public void onCreate() { model = new Board(); }
public void onPause() { }
public void onResume() { }
public void onDestroy() { }
/**
* When the user selects a cell, our presenter only hears about
* what was (row, col) pressed, it's up to the view now to determine that from
* the Button that was pressed.
*/
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());
}
}
}
/**
* When we need to reset, we just dictate what to do.
*/
public void onResetSelected() {
view.clearWinnerDisplay();
view.clearButtons();
model.restart();
}
}
为了让这些表现层和activity无关,我们创建了一个接口并让activity来实现。在测试中,我们基于这个接口来创建模拟值来测试视图层和表现层的交互。
public interface TicTacToeView {
void showWinner(String winningPlayerDisplayLabel);
void clearWinnerDisplay();
void clearButtons();
void setButtonText(int row, int col, String text);
}
进化
更加清晰。我们可以轻松地对表现层逻辑进行单元测试,因为它不和任何Android特定的视图或API相关。也让我们可以测试其他任何视图层,只要视图层实现了TicTacToeView接口。
视图层的问题
维护—表现层和控制层一样,随着时间的推移很容易积攒额外的业务逻辑。在某种程度上,开发者经常发现需要处理笨重的表现层并难以将其拆分。
当然,随着应用程序的改变,细心的开发者可以通过用心的保护来帮助防止这一点。但是,MVVM可以轻易地解决这些。
MVVM
MVVM通过Android中的数据绑定来让测试盒模块化更加容易,同时也加少了我们在关联视图层和模型层的代码。
让我们来看看MVVM的部分。
Model
和MVC一样,没有变化。
View
视图层绑定到被观察的数据,行为被ViewModel以一种灵活的方式展现。
ViewModel
ViewModel的职责是包装模型层并且准备视图层需要观察的数据。它也提供了视图层传递事件到模型层的通道。然而ViewModel和视图层无关。
High level breakdown for Tic Tac Toe.
让我们仔细看看不同的部分,先从ViewModel开始。
public class TicTacToeViewModel implements ViewModel {
private Board model;
/*
* These are observable variables that the viewModel will update as appropriate
* The view components are bound directly to these objects and react to changes
* immediately, without the ViewModel needing to tell it to do so. They don't
* have to be public, they could be private with a public getter method too.
*/
public final ObservableArrayMap<String, String> cells = new ObservableArrayMap<>();
public final ObservableField<String> winner = new ObservableField<>();
public TicTacToeViewModel() {
model = new Board();
}
// As with presenter, we implement standard lifecycle methods from the view
// in case we need to do anything with our model during those events.
public void onCreate() { }
public void onPause() { }
public void onResume() { }
public void onDestroy() { }
/**
* An Action, callable by the view. This action will pass a message to the model
* for the cell clicked and then update the observable fields with the current
* model state.
*/
public void onClickedCellAt(int row, int col) {
Player playerThatMoved = model.mark(row, col);
cells.put("" + row + col, playerThatMoved == null ?
null : playerThatMoved.toString());
winner.set(model.getWinner() == null ? null : model.getWinner().toString());
}
/**
* An Action, callable by the view. This action will pass a message to the model
* to restart and then clear the observable data in this ViewModel.
*/
public void onResetSelected() {
model.restart();
winner.set(null);
cells.clear();
}
}
两个视图层的摘要来显示变量和行为是如何绑定的。
<!--
With Data Binding, the root element is <layout>. It contains 2 things.
1. <data> - We define variables to which we wish to use in our binding expressions and
import any other classes we may need for reference, like android.view.View.
2. <root layout> - This is the visual root layout of our view. This is the root xml tag in the MVC and MVP view examples.
-->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<!-- We will reference the TicTacToeViewModel by the name viewModel as we have defined it here. -->
<data>
<import type="android.view.View" />
<variable name="viewModel" type="com.acme.tictactoe.viewmodel.TicTacToeViewModel" />
</data>
<LinearLayout...>
<GridLayout...>
<!-- onClick of any cell in the board, the button clicked will invoke the onClickedCellAt method with its row,col -->
<!-- The display value comes from the ObservableArrayMap defined in the ViewModel -->
<Button
style="@style/tictactoebutton"
android:onClick="@{() -> viewModel.onClickedCellAt(0,0)}"
android:text='@{viewModel.cells["00"]}' />
...
<Button
style="@style/tictactoebutton"
android:onClick="@{() -> viewModel.onClickedCellAt(2,2)}"
android:text='@{viewModel.cells["22"]}' />
</GridLayout>
<!-- The visibility of the winner view group is based on whether or not the winner value is null.
Caution should be used not to add presentation logic into the view. However, for this case
it makes sense to just set visibility accordingly. It would be odd for the view to render
this section if the value for winner were empty. -->
<LinearLayout...
android:visibility="@{viewModel.winner != null ? View.VISIBLE : View.GONE}"
tools:visibility="visible">
<!-- The value of the winner label is bound to the viewModel.winner and reacts if that value changes -->
<TextView
...
android:text="@{viewModel.winner}"
tools:text="X" />
...
</LinearLayout>
</LinearLayout>
</layout>
高级技巧:多使用tools属性。在上面的例子中,注意到它被用来显示赢家的数值和可见设定。如果你不设置这些,在设计阶段就不容易看出样式如何。
一个关于MVVM和Android数据绑定的笔记。这仅仅是一个数据绑定的简单例子。我强烈推荐你查看Android数据绑定的文档,以了解更多关于这个强大的工具。
进化
现在单元测试甚至更加简单了,因为你完全不用依赖视图层了。当测试时,你只要确认当模型改变时被观察的变量设置的是否合适。没有必要再像MVP模式视图层测试中那样模拟数据。
MVVM的问题
维护—由于视图层可以被同时绑定到变量和表达式上,随着时间推移,无关的表现逻辑会渐渐地加到我们的XML代码中。为了避免这些,从ViewModel中直接获取数值比试图从视图层绑定的表达式中计算或派生要好。这样计算也可以被单元测试覆盖。
结尾
MVP和MVVM在将你的应用程序模块化,单一目的组件上要更好,但是也让你的应用程序更加复杂。对于一个只有一两个页面的简单应用程序,MVC可能做的不错。使用数据绑定的MVVM更遵循响应式编程模型且代码更少,因此更加引人注目。
所以那种模式对你来说最好?如果你基于个人的喜好来在MVP,MVVM之间的作出抉择,那么它们的表现将帮助你理解和权衡。
如果你想要看更多关于MVP和MVVM的实例,我推荐你看看Google Architecture Blueprints项目。也有很多博文深入阐述了MVP的实现。