Android: ¿Qué es MVC, MVP y MVVM?
Tiempo de lectura: 9.28 minutos
En los últimos años han surgido diferentes enfoques sobre cómo organizar los proyectos de desarrollo Android:
La comunidad se ha alejado del patrón MVC (Model View Controller) para dar cabida a patrones más modulares, y que pueden ser testeados con mayor facilidad.
Model View Presenter (MVP) y Model View ViewModel (MVVM) son 2 de las alternativas más ampliamente adoptadas. Sin embargo, los desarrolladores siempre discuten sobre qué enfoque es mejor para el desarrollo Android.
En los últimos años se han publicado varios artículos, donde se aboga que un enfoque es superior al otro.
Sin embargo, muchas veces las opiniones están influenciadas por las propias preferencias del autor, quienes no siempre presentan su crítica de manera objetiva.
En lugar de discutir sobre qué enfoque es mejor, este artículo analiza (objetivamente) las ventajas y los posibles inconvenientes con cada uno de los enfoques, para que puedas tomar una decisión informada por ti mismo.
A fin de ver cada patrón en acción, vamos a usar como ejemplo al clásico juego del Tic-Tac-Toe (o 3 en raya).
Vamos a ver los patrones en este orden: MVC, MVP, y MVVM.
Al inicio de cada sección vamos a empezar con una definición de los componentes y sus responsabilidades, y luego vamos a ver cómo aplicar cada patrón a nuestro juego.
Vamos a empezar. Pero antes, 2 detalles que debes tener en cuenta:
-
El código fuente está disponible en GitHub. En el repositorio encontrarás una rama por cada patrón (por lo tanto puedes hacer
git checkout mvc
,git checkout mvp
, ogit checkout mvvm
para analizar el código de cada sección). - Este artículo está basado en esta publicación original (en inglés), publicada en el blog de Realm y escrita por Eric Maxwell. No es una traducción literal, sino más bien una adaptación.
MVC
El enfoque model, view, controller separa nuestra aplicación, a nivel general, en un conjunto de 3 responsabilidades.
Model
El modelo se constituye por los datos, el estado y la lógica de negocio, de nuestra aplicación Tic-Tac-Toe.
No está vinculado a la vista ni al controlador, y gracias a esto, es reutilizable en muchos contextos.
View
La vista es la representación del modelo.
La vista tiene la responsabilidad de presentar la interfaz de usuario (UI) y comunicarse con el controlador a medida que el usuario interactúa con la aplicación.
En la arquitectura MVC, se dice que las vistas son generalmente "tontas" ya que no tienen conocimiento del modelo. No comprenden el estado o qué hacer cuando un usuario interactúa (haciendo clic en un botón, escribiendo un valor, etc).
Cuanto menos sepan las vistas, menos acopladas estarán (respecto al modelo y controlador), y por lo tanto, serán más flexibles ante cambios.
Controller
El controlador es el pegamento que une la aplicación.
Los controladores determinan lo que sucede en la aplicación.
Cuando la Vista le dice al controlador que un usuario hizo clic en un botón, el controlador decide cómo interactuar con el modelo correspondiente.
Según el cambio de datos en el modelo, el controlador decide si actualizar el estado de la vista o no, según considere apropiado.
En el caso de una aplicación Android, el controlador casi siempre está representado por una Activity o un Fragment.
Representación
Así es cómo se ve nuestra aplicación a nivel general y las clases que representan cada parte:
Examinemos el controlador con mayor detalle:
public class TicTacToeActivity extends AppCompatActivity {
private Board model;
/* View Components (vistas a las que accede el controlador) */
private ViewGroup buttonGrid;
private View winnerPlayerViewGroup;
private TextView winnerPlayerLabel;
/**
* En el método onCreate obtenemos referencias de nuestros view components
* e instanciamos el modelo.
*/
@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();
}
/**
* Aquí inflamos nuestro menú desde su XML. Contiene un botón de reinicio.
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_tictactoe, menu);
return true;
}
/**
* Aquí asociamos el método reset() con el evento producido por tocar el botón de reinicio.
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_reset:
reset();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
/**
* Cuando la vista nos dice que se hizo clic sobre una celda del tic tac toe,
* este método es llamado. Aquí actualizamos el modelo y luego preguntamos por su estado,
* para decidir cómo proceder. Si X u O ganó con este movimiento, actualizamos la vista
* para mostrar ello. De caso contrario sólo marcamos la celda sobre la que se hizo clic.
*/
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);
}
}
}
/**
* Al reiniciar, limpiamos el label del ganador y lo ocultamos, y limpiamos cada botón.
* También le decimos al modelo que reinicie su estado.
*/
private void reset() {
winnerPlayerViewGroup.setVisibility(View.GONE);
winnerPlayerLabel.setText("");
model.restart();
for (int i = 0; i < buttonGrid.getChildCount(); i++) {
((Button) buttonGrid.getChildAt(i)).setText("");
}
}
}
Comentarios sobre el patrón MVC
MVC hace un gran trabajo al separar modelo y vista. Es sencillo escribir pruebas con relación al modelo, ya que no está atado a nada, y no hay mucho que testear en la vista, a nivel de pruebas unitarias.
El controlador sin embargo tiene algunos problemas.
Problemas con el Controlador
- Testability - El controlador está tan vinculado a las APIs de Android que es dificultoso escribir pruebas unitarias.
- Modularity & Flexibility - Los controladores están altamente acoplados a las vistas. De hecho son como una extensión de ellas. Si cambiamos la Vista, tenemos que regresar y hacer cambios en el Controlador.
- Maintenance - Con el tiempo, particularmente en aplicaciones con modelos anémicos, cada vez más código comienza a transferirse a los controladores, haciéndolos muy extensos y "quebradizos".
¿Cómo podemos mejorar esto? ¡MVP al rescate!
MVP
Para MVP sí existe la relación natural view/activity, sin definir esto como una responsabilidad adicional del “controlador”.
Veamos más a continuación, pero comencemos nuevamente con una definición de las responsabilidades, tal como hicimos con MVC.
Model
Lo mismo que para MVC. No hay cambios.
View
El cambio aquí es que ahora la clase Activity/Fragment se considera una parte de la vista. Dejamos de luchar contra la tendencia natural de que vayan de la mano.
Una buena práctica es que la Activity (o Fragment) implemente una interfaz, de tal manera que el presenter haga referencia a ella. Esto elimina el acoplamiento hacia una vista específica y permite hacer pruebas unitarias (con una implementación simulada de la vista).
Presenter
Es similar al controlador de MVC, sólo que no está vinculado a una implementación específica de la Vista, solo a una interfaz. Esto soluciona los problemas de testabilidad, así como de modularidad/flexibilidad que teníamos con MVC. De hecho, puristas del MVP argumentarían que el presenter nunca debería tener referencias a ninguna API o código de Android.
Representación
Una vez más, examinemos cómo luce nuestra aplicación. En este caso usando MVP.
Si observamos al Presenter con más detalle a continuación, veremos cuán simple y clara es ahora la intención de cada acción. En lugar de decirle a la vista "cómo" mostrar algo, simplemente le dice "qué" hacer.
public class TicTacToePresenter implements Presenter {
private TicTacToeView view;
private Board model;
public TicTacToePresenter(TicTacToeView view) {
this.view = view;
this.model = new Board();
}
// Métodos a implementar de acuerdo al ciclo de vida de las Activity en Android.
// Estos métodos están definidos en la interfaz Presenter que esta clase implementa.
public void onCreate() { model = new Board(); }
public void onPause() { }
public void onResume() { }
public void onDestroy() { }
/**
* Cuando un usuario selecciona una celda, nuestro presenter sólo recibe
* como información la fila y columna. La vista es la encargada de determinar tales valores,
* a partir del Button que se presionó.
*/
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());
}
}
}
/**
* Cuando necesitamos resetear, simplemente dictamos qué hacer.
*/
public void onResetSelected() {
view.clearWinnerDisplay();
view.clearButtons();
model.restart();
}
}
A fin de no crear acoplamiento entre activity
y presenter
, creamos una interfaz que la clase Activity implemente.
En una prueba automatizada, usaríamos una implementación simulada de la vista, basados en esta interfaz.
public interface TicTacToeView {
void showWinner(String winningPlayerDisplayLabel);
void clearWinnerDisplay();
void clearButtons();
void setButtonText(int row, int col, String text);
}
Comentarios sobre el patrón MVP
El resultado es mucho más limpio.
Podemos testear fácilmente la lógica del presenter porque no está vinculada a ninguna vista ni API específicas de Android.
Esto también nos permite reemplazar fácilmente la vista por cualquier otra que implemente la interfaz TicTacToeView
.
Problemas con el Presenter
- Maintenance - Los Presenters, al igual que los Controllers, son propensos a recopilar lógica de negocio con el tiempo. En proyectos de larga duración, los desarrolladores a menudo se encuentran con grandes Presenters, difíciles de separar.
Por supuesto, si somos cuidadosos podemos prevenir esto, evitando caer en la tentación a medida que la aplicación cambia con el tiempo. Sin embargo, MVVM puede ayudarnos a abordar esto "haciendo menos" para comenzar.
MVVM
MVVM, con Data Binding en Android, tiene los beneficios de facilitar las pruebas y la modularidad, reduciendo a su vez la cantidad de código que tenemos que escribir para conectar vista + modelo.
Examinemos las partes de MVVM.
Model
Lo mismo que para MVC y MVP. No hay cambios.
View
La vista se vincula con variables "observables" y "acciones" expuestas por el ViewModel de forma flexible.
¿Qué es el ViewModel?
ViewModel
Es el responsable de ajustar el modelo y preparar los datos observables que necesita la vista.
También proporciona hooks
para que la vista pase eventos al modelo.
Sin embargo, el ViewModel no está vinculado a la vista.
Representación
Aquí un desglose a alto nivel para nuestra aplicación Tic Tac Toe.
Echemos un vistazo más de cerca a las partes que aquí intervienen, empezando por nuestra clase ViewModel.
public class TicTacToeViewModel implements ViewModel {
private Board model;
/*
* Estas son variables observables que el ViewModel actualizará según corresponda.
* Los componentes visuales están vinculandos directamente a estos objetos y reaccionan ante cambios
* inmediatamente, sin que el ViewModel les diga qué hacer. No tienen que tener
* visibilidad public; pueden ser private con un método getter que facilite el acceso.
*/
public final ObservableArrayMap<String, String> cells = new ObservableArrayMap<>();
public final ObservableField<String> winner = new ObservableField<>();
public TicTacToeViewModel() {
model = new Board();
}
// Tal como ocurre con el presenter, implementamos métodos asociados al ciclo de vida,
// en caso que necesitemos hacer algo con nuestros modelos durante tales eventos.
public void onCreate() { }
public void onPause() { }
public void onResume() { }
public void onDestroy() { }
/**
* Una acción (Action), llamable desde la vista. Esta acción comunica al modelo
* sobre qué celda se hizo clic, y actualiza los campos observables con
* el estado actual del modelo.
*/
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());
}
/**
* Otra acción (Action), llamable desde la vista. Esta acción solicita al modelo
* resetear su estado, y limpia la data observable en este ViewModel.
*/
public void onResetSelected() {
model.restart();
winner.set(null);
cells.clear();
}
}
A continuación una parte del XML, para ver cómo se vinculan las variables y acciones en la vista:
<!--
Con Data Binding, el elemento raíz es <layout>. Y contiene 2 partes en su interior.
1. <data> - Definimos aquí las variables sobre las que vamos a definir nuestras binding expressions, e
importamos las clases que podríamos necesitar como referencia, tal como android.view.View.
2. <root layout> - Este es el elemento raíz que usaremos como layout de nuestra vista.
Es la etiqueta xml raíz equivalente a los ejemplos de MVC y MVP.
-->
<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">
<!-- Vamos a hacer referencia a TicTacToeViewModel con el nombre viewModel, tal como lo definimos aquí. -->
<data>
<import type="android.view.View" />
<variable name="viewModel" type="com.acme.tictactoe.viewmodel.TicTacToeViewModel" />
</data>
<LinearLayout...>
<GridLayout...>
<!-- Evento onClick para cada celda del tablero. Cada botón invoca al método
onClickedCellAt con los valores row y col correspondientes.
El valor mostrado proviene del ObservableArrayMap definido en el 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>
<!-- La visibilidad del LinearLayour que muestra al ganador depende de si "winner" es null o no.
Se debe tener cuidado de no agregar lógica de presentación a la vista. Para este caso
tiene sentido que el valor de "visibility" dependa de la expresión. Sería extraño que la vista renderice
esta sección si el valor de winner está vacío. -->
<LinearLayout...
android:visibility="@{viewModel.winner != null ? View.VISIBLE : View.GONE}"
tools:visibility="visible">
<!-- El valor de este texto está vinculado a viewModel.winner y reacciona si dicho valor cambia. -->
<TextView
...
android:text="@{viewModel.winner}"
tools:text="X" />
...
</LinearLayout>
</LinearLayout>
</layout>
Tip: Los atributos "tools" sobreescriben valores a fin de previsualizar elementos. Los hemos usado en el ejemplo anterior para mostrar un valor para "winner" y para "visibility". Sin ellos sería complicado ver qué está sucediendo mientras se diseña.
Y una nota al margen de lo que estamos viendo:
Esto es sólo una pequeña muestra de todo lo que se puede hacer con Data Binding. Te recomiendo consultar la documentación de Data Binding para aprender más acerca de esta gran herramienta. También hay un enlace al final de esta página, hacia el proyecto Google Android Architecture Blueprints para que puedas consultar más ejemplos de MVVM y Data Binding.
Comentarios sobre el patrón MVVM
Ahora es incluso más sencillo ejecutar "unit tests", porque realmente no existe una dependencia con la vista. Al hacer pruebas, sólo necesitamos verificar que las variables observables son asignadas apropiadamente cuando el modelo cambia. No hay necesidad de simular una vista para hacer testing como ocurre con el patrón MVP.
Consideraciones sobre MVVM
- Maintenance - Dado que las vistas pueden enlazar variables y expressions, la lógica de presentación puede tornarse inadecuada con el tiempo, afectando a nuestro XML. Para evitar esto, siempre debemos obtener los valores directamente del ViewModel, en lugar de intentar calcularlos a través de
binding expressions
en la vista. De esta manera será posible escribir pruebas unitarias acerca de estos valores calculados.
Conclusiones
Tanto MVP como MVVM hacen un mejor trabajo que MVC al dividir nuestra aplicación en componentes modulares, con propósitos mejor definidos.
Ambos, sin embargo, agregan más complejidad a nuestra aplicación. Para una aplicación simple, con sólo una o 2 pantallas, MVC es suficiente.
MVVM con data binding
es una propuesta atractiva, ya que sigue un modelo de programación más reactivo y produce menos código.
Entonces, ¿qué patrón es mejor para ti?
Si estás escogiendo entre MVP y MVVM, la decisión se reduce más a una preferencia personal, pero ver a ambos en acción te ayudará a entender mejor los beneficios y desventajas.
Si estás interesado en ver más ejemplos prácticos de MVP y MVVM, te recomiendo ver el proyecto Google Architecture Blueprints.