Usando MVP e inversión de dependencias para abstraernos del framework en Android

Usando MVP e inversión de dependencias para abstraernos del framework en Android
Sin comentarios Facebook Twitter Flipboard E-mail

Desde hace unos años se habla mucho sobre temas de arquitectura en Android, y quien más quien menos ha oído hablar de MVP (Model-View-Presenter), y de cómo usarlo para hacer las vistas (normalmente Activities o Fragments) lo más simples posibles, mientras el Presenter se encarga de toda la lógica de presentación.

Normalmente, una de las reglas que suelen imponerse es que desde el Presenter hacia abajo, ninguna de las capas sepa nada sobre Android, para aislarnos de ella, con todos los beneficios que ello conlleva. Seguramente, si te has puesto a ello por primera vez, rápidamente te surgirán dudas sobre cómo implementar esto.

Para poder entenderlo, necesitamos tener claros algunos conceptos.

¿Qué es MVP?

MVP es un patrón arquitectónico que sirve para modelar la capa de presentación del software, esa con la que interactúa el usuario. Lo que hace este patrón es delegar toda la lógica a una entidad llamada Presenter, que será la que se encargue de decidir qué se pinta en la vista y actuar ante los eventos del usuario.

La vista no será más que el punto de interacción con el usuario y, como comentaba, en Android estará representado por una Activity o un Fragment normalmente.

El modelo es básicamente "todo lo demás": toda la lógica de negocio, persistencia de datos, conexión con APIS, etc.

MVP no es una arquitectura

Si has leído sobre este patrón alguna vez, seguramente habrás visto que se suele decir (acertadamente por otro lado) que MVP no se puede considerar una arquitectura por sí misma pues, como comentaba antes, sólo modela la capa de presentación.

Teniendo esto claro, hay que decir también que muchas Apps en Android son lo suficientemente simples (la mayoría simplemente leen una API y la pintan en pantalla) como para que nos podamos plantear sólo estas 3 capas dentro de nuestra aplicación.

En esto de la arquitectura no hay reglas fijas, así que sólo la lógica (y la experiencia) te ayudarán a saber si es suficiente o necesitas algo más complejo como una arquitectura clean.

Las ventajas de usar MVP

MVP nos permite independizarnos del framework

¿Para qué tanto lío? Una de las ventajas con las que se suele vender MVP es que al aislarnos de la capa de presentación, podríamos fácilmente cambiar la vista y mantener el Presenter y el modelo, teniendo un código altamente reutilizable.

La realidad es que, en la práctica, un Presenter casi nunca se reutiliza, porque acaba siendo muy dependiente de la vista con la que interactúa. Es prácticamente imposible tener las mismas pantallas en dos Apps distintas (véase web y Android por ejemplo), y que todas las interacciones sean similares. En Android, además, los Presenters suelen depender bastante del ciclo de vida de las Activities. Yo no he trabajado en ningún proyecto en el que estos Presenters se hayan reutilizado.

¿Entonces que nos aporta? Una característica muy importante, y especialmente útil en un entorno tan invasivo como el de Android: independencia del framework. Gracias a los Presenters, podemos conseguir que a partir de ese punto el SDK de Android no se utilice por ninguna parte.

Esto nos permite:

  • Escribir un código más limpio y semánticamente más correcto (puedes leer un poco sobre Domain Driven Design si te interesan estos temas).
  • Hacer tests unitarios más sencillos sin necesidad de mockear el Framework, algo que ya es posible a día de hoy pero que trae muchos problemas inesperados.
  • Evitar gran parte de tests de instrumentación: al hacer las vistas tan simples, no necesitamos probar prácticamente nada de lógica en ellas. Es verdad que aún así hay mucha parte de visualización que podríamos verificar con herramientas como Espresso, pero estos tests se pueden limitar a lo estrictamente necesario.

La gran duda sobre MVP

Cuando se explica MVP, siempre se suelen utilizar ejemplos bastante sencillos para entender la lógica que hay detrás del patrón. Pero cuando esto se ha comprendido, la mayoría se choca con el siguiente cuello de botella: ¿y cómo uso el contexto desde el Presenter si el Presenter no debería utilizar nada del Framework?

Aquí es donde la inversión de dependencias soluciona el papel.

¿Qué es la inversión de dependencias?

Sin entrar en muchos detalles, ya que esto se explicó en un artículo anterior, la inversión de dependencias consiste en que en vez de crear las dependencias que necesitamos para realizar una tarea, estas nos vendrán dadas desde fuera. La forma de inyectarlas puede ser bien por constructor, bien mediante un setter.

De esta forma, a nuestra entidad (en este caso el Presenter) le da igual el tipo concreto que se nos esté pasando por parámetro, ya que podemos trabajar con una interfaz.

Esto resuelve gran parte de nuestros problemas

Ahora ya no necesitamos pasar el contexto, por ejemplo, para realizar tareas que dependan de él.

Supongamos que necesitamos acceder a las SharedPreferences para almacenar o recuperar un dato. ¿Cómo lo vamos a hacer si la vista no puede tener lógica y el Presenter no puede utilizar el contexto?

Gracias a la inversión de dependencias podemos hacerlo sin problemas, creando una interfaz con la que el *Presenter interactúe. Nuestra implementación utilizará el contexto, pero eso al Presenter ya le es indiferente. En los tests podemos mockear esa interfaz sin problemas para que haga lo que necesitemos.

En vez de crear el objeto dentro del Presenter, a este se le proveerá mediante el constructor o un setter, de tal forma que el Presenter no necesita conocer el tipo concreto que está utilizando.

Pero complica mucho la provisión de dependencias

Efectivamente, cuando tenemos un único caso como el anterior, la vista puede proveer de la dependencia al Presenter, pero si aplicamos esta lógica en toda nuestra aplicación, se nos puede ir de las manos. Se vuelve cada vez más y más difícil saber cómo y dónde crear nuestros objetos, y cómo setearlos correctamente.

Aquí es donde surge el concepto de inyector de dependencias. Se trata de un componente del software que proveerá automáticamente las dependencias al resto de entidades. En Java es muy conocido Guice, pero si buscamos algo más optimizado para Android, el inyector estrella es Dagger.

No quiero entrar en más detalles en este punto, pero si quieres conocer un poco más sobre el tema, puedes echar un vistazo a este blog donde explican a fondo cómo usar Dagger 2.

Un ejemplo

Siguiendo la idea anterior, voy a poner un ejemplo muy simple de cómo funcionaría esto.

Vamos a crear una App con una Activity que en el onResume guarde una preferencia indicando que está activa y en el onPause la actualice. En este ejemplo no voy a incluir el inyector de dependencias por simplicidad, así que será la vista la que provea las dependencias.

Para ello, necesitaremos esa interfaz que va a evitar que tengamos que pasar el contexto al Presenter:


public interface PreferencesManager {

    void saveBooleanPreference(String key, boolean value);

    boolean retrieveBooleanPreference(String key);
}

El Presenter puede utilizar la interfaz sin conocer la clase concreta que lo implementa. Básicamente estamos creando una fachada sobre las SharedPreferences de Android:


public class MainPresenter {
    
    private static final String ACTIVITY_RESUMED = "activityResumed";
    private PreferencesManager preferencesManager;

    public void onResume() {
        preferencesManager.saveBooleanPreference(ACTIVITY_RESUMED, true);
    }
    
    public void onPause() {
        preferencesManager.saveBooleanPreference(ACTIVITY_RESUMED, false);
    }
}

¿Cómo le asignamos un valor? Puedes hacerlo por ejemplo a través del constructor. Esta opción te da la ventaja de la inmutabilidad.


    private final PreferencesManager preferencesManager;

    public MainPresenter(PreferencesManager preferencesManager) {
        this.preferencesManager = preferencesManager;
    }

Esto ya nos permite mockear la interfaz en los tests con una herramienta como Mockito, y hacer que para nuestros tests unitarios la clase devuelva lo que queramos.

Pero además, nos hemos librado de tener que tratar con el Framework de Android en el Presenter, que ahora es puramente Java.

Por otra parte, necesitaremos una clase que implemente esa interfaz, que podría ser por ejemplo:


public class AndroidPrefsManager implements PreferencesManager {
    
    private final SharedPreferences prefs;

    public AndroidPrefsManager(Context context) {
        prefs = context.getSharedPreferences("prefs", Context.MODE_PRIVATE);
    }

    @Override public void saveBooleanPreference(String key, boolean value) {
        prefs.edit().putBoolean(key, value).apply();
    }

    @Override public boolean retrieveBooleanPreference(String key) {
        return prefs.getBoolean(key, false);
    }
}

Por último, ya que no tenemos inyector de dependencias, la activity se encargará de crear y asignar las instancias:


public class MainActivity extends AppCompatActivity {

    private MainPresenter presenter;

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

        PreferencesManager preferencesManager = new AndroidPrefsManager(this);
        presenter = new MainPresenter(preferencesManager);
    }

    @Override protected void onResume() {
        super.onResume();
        presenter.onResume();
    }

    @Override protected void onPause() {
        super.onPause();
        presenter.onPause();
    }
}

Utilizando esta misma idea, y dejando en un único módulo las clases que implementen las interfaces que necesitemos y que requieran algo del framework de Android, podemos aislarnos de forma relativamente sencilla de ello.

Es un punto importante intentar que estas clases sean lo más pequeñas posibles, ya que nos resultarán más complicadas de testear, así que cuanta menos lógica tengan mucho mejor.

Por supuesto, este es el siguiente paso a la hora de comprender cómo aplicar MVP en Android, y aún sigue siendo una simplificación de una situación real. Comenta tus dudas en los comentarios, y podrán ser objeto de un próximo artículo sobre el tema.

Comentarios cerrados
Inicio