1. Code
  2. Coding Fundamentals
  3. Design Patterns

Cómo adoptar Model View Presenter en Android

En el tutorial anterior, hablamos sobre el modelo Model View Presenter, cómo se aplica en Android y cuáles son sus ventajas más importantes. En este tutorial, exploramos el patrón Model View Presenter con más detalle implementándolo en una aplicación de Android.
Scroll to top
This post is part of a series called How to Adopt Model View Presenter on Android.
An Introduction to Model View Presenter on Android
Testing and Dependency Injection With Model View Presenter on Android

Spanish (Español) translation by Elías Nicolás (you can also view the original English article)

En el tutorial anterior, hablamos sobre el modelo Model View Presenter, cómo se aplica en Android y cuáles son sus ventajas más importantes. En este tutorial, exploramos el patrón Model View Presenter con más detalle implementándolo en una aplicación de Android.

En este tutorial:

  • Construimos una aplicación simple usando el patrón MVP
  •  Exploramos cómo implementar el patrón MVP en Android
  • Y discutimos cómo superar algunas dificultades causadas por la arquitectura de Android

1. Model View Presenter

El patrón Model View Presenter es un patrón arquitectónico basado en el patrón Model View Controller (MVC) que aumenta la separación de las preocupaciones y facilita las pruebas unitarias. Crea tres capas, Model, View y Presenter, cada una con una responsabilidad bien definida.

Model View Presenter LayersModel View Presenter LayersModel View Presenter Layers

El Modelo contiene la lógica de negocio de la aplicación. Controla cómo se crean, almacenan y modifican los datos. View es una interfaz pasiva que muestra datos y dirige las acciones del usuario al presentador Presenter. El Presentador actúa como intermediario. Recupera datos del modelo y lo muestra en View. También procesa las acciones de usuario enviadas por la vista.

2. Planificación y configuración del proyecto

Vamos a construir una aplicación de notas sencilla para ilustrar MVP. La aplicación permite al usuario tomar notas, guardarlas en una base de datos local y eliminar notas. Para que sea sencillo, la aplicación tendrá una sola actividad.

App LayoutApp LayoutApp Layout

En este tutorial, nos concentramos principalmente en la implementación del patrón MVP. Otras funciones, como la creación de una base de datos SQLite, la construcción de un DAO o el manejo de la interacción del usuario, se saltan. Si necesita ayuda con cualquiera de estos temas, Envato Tuts+ tiene algunos excelentes tutoriales sobre estos temas.

Diagrama de acción y capas MVP

Comencemos con la creación de una nueva nota. Si rompemos esta acción en operaciones más pequeñas, entonces este es el aspecto que tendría el flujo usando el patrón arquitectónico MVP:

  • El usuario escribe una nota y hace clic en el botón añadir nota.
  • El presentador crea un objeto Note con el texto introducido por el usuario y le pide al modelo que lo inserte en la base de datos.
  • El modelo inserta la nota en la base de datos e informa al presentador de que la lista de notas ha cambiado.
  • El presentador borra el campo de texto y le pide a la vista que actualice su lista para mostrar la nota recién creada.
MVP Action DiagramMVP Action DiagramMVP Action Diagram

Interfaces MVP

Consideremos ahora las operaciones necesarias para lograr esta acción y separarlas usando MVP. Para mantener los diversos objetos ligeramente acoplados, la comunicación entre las capas toma lugar utilizando interfaces. Necesitamos cuatro interfaces:

  • RequiredViewOps: necesario Ver operaciones disponibles para Presenter
  • ProvidedPresenterOps: operaciones ofrecidas a View para comunicación con Presenter
  • RequiredPresenterOps: requiere las operaciones de Presenter disponibles para Model
  • ProvidedModelOps: operaciones ofrecidas a Model para comunicarse con Presenter
MVP InterfacesMVP InterfacesMVP Interfaces

3. Implementación de MVP en Android

Ahora que tenemos una idea de cómo los diversos métodos deben ser organizados, podemos comenzar a crear nuestra aplicación. Simplificamos la implementación concentrándonos únicamente en la acción para agregar una nueva nota. Los archivos fuente de este tutorial están disponibles en GitHub.

Utilizamos solamente una actividad Activity con un diseño que incluye:

  • EditText para nuevas notas
  • Button para añadir una nota
  • RecyclerView para listar todas las notas
  • Dos elementos TextView y un botón Button dentro de un contenedor de RecyclerView

Interfaces

Comencemos creando las interfaces. Para mantener todo organizado, ponemos las interfaces dentro de un titular. Una vez más, en este ejemplo nos centramos en la acción para agregar una nueva nota.

1
public interface MVP_Main {
2
    /**

3
     * Required View methods available to Presenter.

4
     * A passive layer, responsible to show data

5
     * and receive user interactions

6
     */
7
    interface RequiredViewOps {
8
      // View operations permitted to Presenter

9
		Context getAppContext();
10
        	Context getActivityContext();
11
		void notifyItemInserted(int layoutPosition);
12
        	void notifyItemRangeChanged(int positionStart, int itemCount);
13
    }
14
15
    /**

16
     * Operations offered to View to communicate with Presenter.

17
     * Processes user interactions, sends data requests to Model, etc.

18
     */
19
    interface ProvidedPresenterOps {
20
       	// Presenter operations permitted to View

21
		void clickNewNote(EditText editText);
22
            // setting up recycler adapter

23
            int getNotesCount();
24
            NotesViewHolder createViewHolder(ViewGroup parent, int viewType);
25
            void bindViewHolder(NotesViewHolder holder, int position);
26
    }
27
28
    /**

29
     * Required Presenter methods available to Model.

30
     */
31
    interface RequiredPresenterOps {
32
       	// Presenter operations permitted to Model

33
		Context getAppContext();
34
        	Context getActivityContext();
35
    }
36
37
    /**

38
     * Operations offered to Model to communicate with Presenter

39
     * Handles all data business logic.

40
     */
41
    interface ProvidedModelOps {
42
        	// Model operations permitted to Presenter

43
            int getNotesCount();
44
            Note getNote(int position);
45
		int insertNote(Note note);
46
        	boolean loadData();
47
    }
48
}

Capa View

Ahora es el momento de crear las capas Model, View y Presenter. Dado que MainActivity funcionará como View, debería implementar la interfaz RequiredViewOps.

1
public class MainActivity
2
        extends AppCompatActivity
3
    implements View.OnClickListener, MVP_Main.RequiredViewOps {
4
5
    private MVP_Main.ProvidedPresenterOps mPresenter;
6
    private EditText mTextNewNote;
7
    private ListNotes mListAdapter;
8
9
    @Override
10
    public void onClick(View v) {
11
        switch (v.getId()) {
12
            case R.id.fab:{
13
                // Adds a new note

14
                mPresenter.clickNewNote(mTextNewNote);
15
            }
16
        }
17
    }
18
    @Override
19
    public Context getActivityContext() {
20
        return this;
21
    }
22
23
    @Override
24
    public Context getAppContext() {
25
        return getApplicationContext();
26
    }
27
    // Notify the RecyclerAdapter that a new item was inserted

28
    @Override
29
    public void notifyItemInserted(int adapterPos) {
30
        mListAdapter.notifyItemInserted(adapterPos);
31
    }
32
    // notify the RecyclerAdapter that items has changed

33
    @Override
34
    public void notifyItemRangeChanged(int positionStart, int itemCount){
35
        mListAdapter.notifyItemRangeChanged(positionStart, itemCount);
36
    }
37
    // notify the RecyclerAdapter that data set has changed

38
    @Override
39
    public void notifyDataSetChanged() {
40
        mListAdapter.notifyDataSetChanged();
41
    }
42
    // Recycler adapter

43
    // This class could have their own Presenter, but for the sake of

44
    // simplicity, will use only one Presenter.

45
    // The adapter is passive and all the processing occurs 

46
    // in the Presenter layer.

47
    private class ListNotes extends RecyclerView.Adapter<NotesViewHolder>       
48
    {
49
        @Override
50
        public int getItemCount() {
51
            return mPresenter.getNotesCount();
52
        }
53
54
        @Override
55
        public NotesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
56
            return mPresenter.createViewHolder(parent, viewType);
57
        }
58
59
        @Override
60
        public void onBindViewHolder(NotesViewHolder holder, int position) {
61
            mPresenter.bindViewHolder(holder, position);
62
        }
63
    }
64
}

Capa Presenter

El presentador es el intermediario y necesita implementar dos interfaces:

  • ProvidedPresenterOps para permitir llamadas desde la vista
  • RequiredPresenterOps para recibir resultados del modelo

Preste especial atención a la referencia de capa Ver. Necesitamos usar WeakReference<MVP_Main.RequiredViewOps> ya que MainActivity podría ser destruida en cualquier momento y queremos evitar pérdidas de memoria. Además, la capa de modelo aún no se ha configurado. Lo hacemos más tarde cuando conectamos las capas MVP.

1
public class MainPresenter implements MVP_Main.ProvidedPresenterOps, MVP_Main.RequiredPresenterOps {
2
3
    // View reference. We use as a WeakReference

4
    // because the Activity could be destroyed at any time

5
    // and we don't want to create a memory leak

6
    private WeakReference<MVP_Main.RequiredViewOps> mView;
7
    // Model reference

8
    private MVP_Main.ProvidedModelOps mModel;
9
10
    /**

11
     * Presenter Constructor

12
     * @param view  MainActivity

13
     */
14
    public MainPresenter(MVP_Main.RequiredViewOps view) {
15
        mView = new WeakReference<>(view);
16
    }
17
18
    /**

19
     * Return the View reference.

20
     * Throw an exception if the View is unavailable.

21
     */
22
    private MVP_Main.RequiredViewOps getView() throws NullPointerException{
23
        if ( mView != null )
24
            return mView.get();
25
        else
26
            throw new NullPointerException("View is unavailable");
27
    }
28
29
    /**

30
     * Retrieves total Notes count from Model

31
     * @return  Notes list size

32
     */
33
    @Override
34
    public int getNotesCount() {
35
        return mModel.getNotesCount();
36
    }
37
38
    /**

39
     * Creates the RecyclerView holder and setup its view

40
     * @param parent    Recycler viewGroup

41
     * @param viewType  Holder type

42
     * @return          Recycler ViewHolder

43
     */
44
    @Override
45
    public NotesViewHolder createViewHolder(ViewGroup parent, int viewType) {
46
        NotesViewHolder viewHolder;
47
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
48
49
        View viewTaskRow = inflater.inflate(R.layout.holder_notes, parent, false);
50
        viewHolder = new NotesViewHolder(viewTaskRow);
51
52
        return viewHolder;
53
    }
54
55
    /**

56
     * Binds ViewHolder with RecyclerView

57
     * @param holder    Holder to bind

58
     * @param position  Position on Recycler adapter

59
     */
60
    @Override
61
    public void bindViewHolder(final NotesViewHolder holder, int position) {
62
        final Note note = mModel.getNote(position);
63
        holder.text.setText( note.getText() );
64
        holder.date.setText( note.getDate() );
65
        holder.btnDelete.setOnClickListener(new View.OnClickListener() {
66
            @Override
67
            public void onClick(View v) {
68
                clickDeleteNote(note, holder.getAdapterPosition(), holder.getLayoutPosition());
69
            }
70
        });
71
72
    }
73
74
    /**

75
     * @return  Application context

76
     */
77
    @Override
78
    public Context getAppContext() {
79
        try {
80
            return getView().getAppContext();
81
        } catch (NullPointerException e) {
82
            return null;
83
        }
84
    }
85
86
    /**

87
     * @return  Activity context

88
     */
89
    @Override
90
    public Context getActivityContext() {
91
        try {
92
            return getView().getActivityContext();
93
        } catch (NullPointerException e) {
94
            return null;
95
        }
96
    }
97
98
    /**

99
     * Called by View when user clicks on new Note button.

100
     * Creates a Note with text typed by the user and asks

101
     * Model to insert it in DB.

102
     * @param editText  EditText with text typed by user

103
     */
104
    @Override
105
    public void clickNewNote(final EditText editText) {
106
        getView().showProgress();
107
        final String noteText = editText.getText().toString();
108
        if ( !noteText.isEmpty() ) {
109
            new AsyncTask<Void, Void, Integer>() {
110
                @Override
111
                protected Integer doInBackground(Void... params) {
112
                    // Inserts note in Model, returning adapter position

113
                    return mModel.insertNote(makeNote(noteText));
114
                }
115
116
                @Override
117
                protected void onPostExecute(Integer adapterPosition) {
118
                    try {
119
                        if (adapterPosition > -1) {
120
                            // Note inserted

121
                            getView().clearEditText();
122
                            getView().notifyItemInserted(adapterPosition + 1);
123
                            getView().notifyItemRangeChanged(adapterPosition, mModel.getNotesCount());
124
                        } else {
125
                            // Informs about error

126
                            getView().hideProgress();
127
                            getView().showToast(makeToast("Error creating note [" + noteText + "]"));
128
                        }
129
                    } catch (NullPointerException e) {
130
                        e.printStackTrace();
131
                    }
132
                }
133
            }.execute();
134
        } else {
135
            try {
136
                getView().showToast(makeToast("Cannot add a blank note!"));
137
            } catch (NullPointerException e) {
138
                e.printStackTrace();
139
            }
140
        }
141
    }
142
143
    /**

144
     * Creates a Note object with given text

145
     * @param noteText  String with Note text

146
     * @return  A Note object

147
     */
148
    public Note makeNote(String noteText) {
149
        Note note = new Note();
150
        note.setText( noteText );
151
        note.setDate(getDate());
152
        return note;
153
154
    }
155
}

Capa de modelo

La capa Modelo es responsable de manejar la lógica. Contiene una ArrayList con las notas agregadas a la base de datos, una referencia DAO para realizar operaciones de base de datos y una referencia al presentador.

1
public class MainModel implements MVP_Main.ProvidedModelOps {
2
3
    // Presenter reference

4
    private MVP_Main.RequiredPresenterOps mPresenter;
5
    private DAO mDAO;
6
    // Recycler data

7
    public ArrayList<Note> mNotes;
8
9
    /**

10
     * Main constructor, called by Activity during MVP setup

11
     * @param presenter Presenter instance

12
     */
13
    public MainModel(MVP_Main.RequiredPresenterOps presenter) {
14
        this.mPresenter = presenter;
15
        mDAO = new DAO( mPresenter.getAppContext() );
16
    }
17
18
     /**

19
     * Inserts a note on DB

20
     * @param note  Note to insert

21
     * @return      Note's position on ArrayList

22
     */
23
    @Override
24
    public int insertNote(Note note) {
25
        Note insertedNote = mDAO.insertNote(note);
26
        if ( insertedNote != null ) {
27
            loadData();
28
            return getNotePosition(insertedNote);
29
        }
30
        return -1;
31
    }
32
33
     /**

34
     * Loads all Data, getting notes from DB

35
     * @return  true with success

36
     */
37
    @Override
38
    public boolean loadData() {
39
        mNotes = mDAO.getAllNotes();
40
        return mNotes != null;
41
    }
42
     /**

43
     * Gets a specific note from notes list using its array position

44
     * @param position    Array position

45
     * @return            Note from list

46
     */
47
    @Override
48
    public Note getNote(int position) {
49
        return mNotes.get(position);
50
    }
51
     
52
    /**

53
     * Get ArrayList size

54
     * @return  ArrayList size

55
     */
56
    @Override
57
    public int getNotesCount() {
58
        if ( mNotes != null )
59
            return mNotes.size();
60
        return 0;
61
    }
62
}

4. Atar todo junto

Con las capas MVP en su lugar, necesitamos instanciarlas e insertar las referencias necesarias. Antes de hacerlo, tenemos que abordar algunos problemas que están directamente relacionados con Android.

Instanciar las capas

Debido a que Android no permite la creación de instancias de una actividad Activity, la capa de vista se instanciará para nosotros. Somos responsables de instanciar las capas del presentador y del modelo. Desafortunadamente, instanciar esas capas fuera de la Actividad Activity puede ser problemática.

Se recomienda utilizar una forma de inyección de dependencia para lograr esto. Dado que nuestro objetivo es concentrarnos en la implementación del MVP, tomaremos un enfoque más fácil. Este no es el mejor enfoque disponible, pero es el más fácil de entender. Discutiremos MVP y la inyección de la dependencia más adelante en esta serie.

  • Instanciar el Presentador y el Modelo en la Actividad usando variables locales
  • Configure RequiredViewOps y ProvidedModelOps en el presentador
  • Configure RequiredPresenterOps en el modelo
  • Guardar ProvidedPresenterOps como referencia para usar en la vista
1
/**

2
* Setup Model View Presenter pattern

3
*/
4
private void setupMVP() {
5
     // Create the Presenter

6
     MainPresenter presenter = new MainPresenter(this);
7
     // Create the Model

8
     MainModel model = new MainModel(presenter);
9
     // Set Presenter model

10
     presenter.setModel(model);
11
     // Set the Presenter as a interface

12
     mPresenter = presenter;
13
}

Manejo de cambios de configuración

Otra cosa que debemos considerar es el ciclo de vida de la actividad. La actividad de Android Activity podría ser destruida en cualquier momento y las capas de Presenter y Model también podrían ser destruidas con ella. Necesitamos solucionar esto usando algún tipo de máquina de estado para guardar el estado durante los cambios de configuración. También debemos informar a las otras capas sobre el estado de la Actividad.

Para lograr esto, utilizaremos una clase separada, StateMaintainer, que contiene un fragmento que mantiene su estado y utiliza este fragmento para guardar y recuperar nuestros objetos. Puedes echar un vistazo a la implementación de esta clase en los archivos de origen de este tutorial.

Necesitamos agregar un método onDestroy al Presentador y al Modelo para informarles sobre el estado actual de la Actividad. También debemos agregar un método setView al Presentador, el cual será responsable de recibir una nueva referencia View de la Actividad recreada.

View LifecycleView LifecycleView Lifecycle
1
public class MainActivity
2
        extends AppCompatActivity
3
    implements View.OnClickListener, MVP_Main.RequiredViewOps
4
{
5
    // …

6
    private void setupMVP() {
7
        // Check if StateMaintainer has been created

8
        if (mStateMaintainer.firstTimeIn()) {
9
            // Create the Presenter

10
            MainPresenter presenter = new MainPresenter(this);
11
            // Create the Model

12
            MainModel model = new MainModel(presenter);
13
            // Set Presenter model

14
            presenter.setModel(model);
15
            // Add Presenter and Model to StateMaintainer

16
            mStateMaintainer.put(presenter);
17
            mStateMaintainer.put(model);
18
19
            // Set the Presenter as a interface

20
            // To limit the communication with it

21
            mPresenter = presenter;
22
23
        }
24
        // get the Presenter from StateMaintainer

25
        else {
26
            // Get the Presenter

27
            mPresenter = mStateMaintainer.get(MainPresenter.class.getName());
28
            // Updated the View in Presenter

29
            mPresenter.setView(this);
30
        }
31
    }
32
    // …

33
}

Conclusión

El patrón MVP es capaz de resolver algunos problemas causados por la arquitectura predeterminada de Android. Hace que su código sea fácil de mantener y probar. La adopción de MVP puede parecer difícil al principio, pero una vez que entienda la lógica detrás de ella, todo el proceso es sencillo.

Ahora puede crear su propia biblioteca MVP o utilizar una solución que ya está disponible, como Mosby o simple-mvp. Ahora deberías entender mejor lo que estas bibliotecas están haciendo detrás de las escenas.

Estamos casi al final de nuestro viaje MVP. En la tercera y última parte de esta serie, agregaremos las pruebas unitarias a la mezcla y adaptaremos nuestro código para usar la inyección de dependencia con Dagger. Espero verte allí.