De MVP a Clean I. ¿Es esto MVP?

La primera vez que decidimos en mi empresa desarrollar una app con MVP, me puse a leer un poco de aquí y otro poco de allá, y empecé a aplicar desconfiado las soluciones que me proponía la comunidad.

Con el paso de los proyectos, el conocimiento iba creciendo, pero las dudas también, y debo decir que las cosas no empezaron a aclararse hasta que profundicé en el proyecto Android Architecture Blueprints. Poco después, un estupendo curso de Jorge Ortiz me ayudó a terminar de encajar las piezas.

Mi intención en este artículo es sacar a la luz las claves del patrón MVP. Tras un tiempo refinando la estructura de mis proyectos, creo que puedo aportar alguna que otra respuesta.
Intentaré ir de lo más abstracto a lo más concreto. 😉

¿Qué es MVP?

Básicamente MVP es un patrón arquitectónico que busca ante todo desacoplar la lógica de presentación de las vistas

Con la finalidad de conseguir una vista lo más “tonta” posible (vista pasiva), una clase, comúnmente conocida como Presenterasumirá la responsabilidad de comunicarse con el Modelo y gestionará toda la lógica necesaria para presentar la información al usuario.

Capas del patrón MVP

Según la Wikipedia:

  • El modelo es una interfaz que define los datos que se mostrará o no actuado en la interfaz de usuario.
  • El presentador actúa sobre el modelo y la vista. Recupera datos de los repositorios (el modelo), y los formatea para mostrarlos en la vista.
  • La vista es una interfaz pasiva que exhibe datos (el modelo) y órdenes de usuario de las rutas (eventos) al presentador para actuar sobre los datos.

Este esquema creo que refleja bien la idea:MVPComo vemos, la Vista informa al Presenter de que algo ha sucedido (el usuario ha pulsado un botón, por ejemplo), en ese momento el Presenter decide que esa acción implica solicitar datos al Modelo, luego el Presenter procesa la información recibida e invoca los métodos de la Vista que considere oportunos.

La clave está en el Presenter

Imaginemos dos abstracciones:

Una que represente a nuestra vista,

public interface DetailMovieView {
  void displayImage(String url);
  void displayTitle(String title);
  void displayVoteAverage(String voteAverage);
  void displayReleaseDate(String releaseDate);
  void displayOverview(String overview);
}

y otra que represente nuestra puerta de entrada a los datos.

interface MoviesRepository {
  void getMovies(Handler<List<Movie>> handler);
  void getMovie(int movieId, Handler<Movie> handler);
}

La primera será implementada por la Activity que pinte la pantalla de detalle, y la segunda por la clase encargada de manejar la lógica de obtención de datos. El Presenter necesita las instancias de sendas interfaces para, por ejemplo, obtener una película o pedir que se muestre su título, pero para ello no tiene por qué conocer sus implementaciones concretas. Ni debe.

public class DetailMoviePresenter implements Handler<Movie> {

  private MoviesRepository repository;
  private Formatter formatter;
  private int movieId;
  private WeakReference<DetailMovieView> view;

  public DetailMoviePresenter(MoviesRepository repository, Formatter formatter, int movieId) {
    this.repository = repository;
    this.formatter = formatter;
    this.movieId = movieId;
  }

  public void setView(DetailMovieView detailMovieView) {
    view = new WeakReference<>(detailMovieView);
  }

  public void viewReady() {
    repository.getMovie(movieId, this);
  }

  @Override
  public void handle(Movie movie) {
    DetailMovieView detailMovieView = view.get();
    if(detailMovieView!=null) {
      detailMovieView.displayImage(formatter.getCompleteUrlImage(movie.getBackdropPath()));
      detailMovieView.displayTitle(movie.getTitle());
      detailMovieView.displayVoteAverage(movie.getVoteAverage());
      detailMovieView.displayReleaseDate(formatter.formatDate(movie.getReleaseDate()));
      detailMovieView.displayOverview(movie.getOverview());
    }
  }

  @Override
  public void error() {}
}

De esta forma conseguimos aislar a la clase que gestionará la lógica de presentación del framework de Android y del acceso a datos.
MVP
Si queremos cumplir con el principio de inversión de dependencias debemos evitar que el Presenter sepa de concreciones. Trabajar directamente con abstracciones nos ayudará a evitar el acoplamiento entre nuestros módulos (al aislarnos de los detalles de implementación), y nos hará más resistentes al cambio (una clase suele ser mucho más volátil que la interfaz que la representa).

Injection

Dagger 2 es sin duda el inyector de dependencias de referencia para Android y Java, y en la segunda parte de este artículo hablaré un poco más a fondo sobre él, pero de momento he preferido optar por una forma más «manual» de proveer las dependencias; no me gustaría que un árbol nos impidiese ver el bosque…

public class Injection {
  public static MoviesRepository provideRepository(Context context) {
    return MoviesRepositoryImpl.getInstance(getLocalDataSource(context), getRemoteDataSource());
  }
  private static MoviesLocalDataSource getLocalDataSource(Context context) {
    return MoviesLocalDataSourceImpl.getInstance(MoviesDatabaseHelper.getInstance(context));
  }
  private static MoviesRemoteDataSource getRemoteDataSource(){
    return MoviesRemoteDataSourceImpl.getInstance(RetrofitClient.getInstance());
  }
}

Como vemos, la clase Injection sabe cómo crear una instancia de MoviesRepository, y de las fuentes de datos (Datasource) de que depende. En el ejemplo que propongo, será la Activity la que creará la instancia del Presenter accediendo a la clase Injection (pasándole como parámetro el contexto de la aplicación).

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_detail_movie);
  setUpPresenter();
}

private void setUpPresenter() {
  int movieId = getIntent().getIntExtra(EXTRA_MOVIE_ID, -1);
  presenter = new DetailMoviePresenter(
    Injection.provideRepository(getApplicationContext()),
    new Formatter(),
    movieId);
  presenter.setView(this);
}

Una vez hecho esto ya tendremos un Presenter independiente. Una clase Java pura, sin ninguna ligazón al framework de Android, que no sabe cómo se obtienen los datos, pero que tiene todo el poder para escuchar cualquier llamada de atención de la vista, solicitar datos, formatear dichos datos o manejar errores.

WeakReference

Si bien es cierto que el uso de referencias débiles a la ligera puede provocar errores inesperados, considero interesante la idea de que la referencia que tiene el Presenter de la Vista sea del tipo WeakReference.

Como sabemos, al girar la pantalla del dispositivo, la vista se destruye y se vuelve a construir; si mantenemos una referencia fuerte a esa vista estaremos impidiendo que el recolector de basura la elimine, siendo más proclives a incurrir en el tan temido fallo de memoria o memory leak.

La solución es dejarla ir. Mantén una referencia débil a la vista, y antes de utilizarla comprueba si todavía existe con el método get().

No te olvides del Adapter

Si queremos que nuestro Presenter gestione la lógica de presentación de toda la vista, no debemos olvidarnos de las listas. Al fin y al cabo, las listas se componen de pequeñas vistas, que tampoco deberían manejar lógica. Esta responsabilidad hay que dejársela al Presenter. Veamos cómo hacerlo:

  • Primero definamos una interfaz que represente al ítem.

public interface MovieCellView {
  void displayImage(String url);
}

  • Luego añadimos un método en el Presenter que sepa cómo pintar el ítem, y otro que nos devuelva el número de elementos (es el Presenter el que tendrá la lista de objetos dinámica).

public void configureCell(MovieCellView movieCellView, int position) {
  Movie movie = movieList.get(position);
  movieCellView.displayImage(formatter.getCompleteUrlImage(movie.getPosterPath()));
}

public int getItemsCount() {
  if(movieList==null || movieList.isEmpty()) {
    return 0;
  } else {
    return movieList.size();
  }
}

  • Por último creamos el Adapter, que carecerá de lógica de presentación, y que tan sólo necesitará una instancia del Presenter para poder trabajar.

public class MoviesAdapter extends RecyclerView.Adapter<MoviesAdapter.MovieHolder> {

  private MovieListPresenter presenter;

  public MoviesAdapter(MovieListPresenter presenter) {
    this.presenter = presenter;
  }

  @Override
  public MovieHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) {
    if (viewGroup instanceof RecyclerView ) {
      View view = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.item_movie, viewGroup, false);
      return new MovieHolder(view);
    } else {
      throw new RuntimeException("Not bound to RecyclerView");
    }
  }

  @Override
  public void onBindViewHolder(MovieHolder movieHolder, int position) {
    presenter.configureCell(movieHolder, position);
  }

  @Override
  public int getItemCount() {
    return presenter.getItemsCount();
  }

  public class MovieHolder extends RecyclerView.ViewHolder implements MovieCellView {

    @BindView(R.id.image) ImageView imageView;

    public MovieHolder(View itemView) {
      super(itemView);
      ButterKnife.bind(this, itemView);
    }

    @Override
    public void displayImage(String url) {
      Picasso.with(imageView.getContext())
      .load(url)
      .placeholder(R.drawable.movie_placeholder)
      .into(imageView);
    }
  }
}

Manejo de errores

He creado una interfaz a modo de callback, que me permite comunicar al Presenter con el Repository, y avisar en caso de que algo vaya mal.

public interface Handler<T> {
  void handle(T result);
  void error();
}

Testing

Ahora que el Presenter está completamente desligado del framework de Android, la cantidad de código testable de forma unitaria aumentará, y una vez mockeadas las instancias de la Vista y el Repository, seremos capaces de verificar las interacciones del Presenter con éstas.

En mi proyecto de ejemplo, me he centrado sobre todo en cubrir con tests todos los caminos lógicos que van desde el Presenter hasta el Repository.

Conclusión

Ya para terminar, me gustaría dejaros 3 puntos que os pueden ayudar a verificar si realmente estáis cumpliendo MVP:

  • El Presenter se comunica con el Modelo a través de una interfaz.
  • La Vista no maneja datos. Es el Presenter el que tiene la referencia al objeto o conjunto de objetos a pintar.
  • La Vista no maneja lógica de presentación. Esto no quiere decir que si tenemos un if-else ya no estamos cumpliendo con el patrón MVP; validar si un Fragment existe en un determinado contenedor, o si la instancia de tipo Bundle (savedInstanceState) de la Activity es igual a null, se podría considerar lógica de vista. Lógica de presentación es, cuando por ejemplo un usuario pulsa un botón, decidir si se muestra un cuadro de diálogo o se navega hacia la siguiente pantalla.

Código fuente

Enlaces de referencia