Статьи

Как принять Model View Presenter на Android

В предыдущем уроке мы говорили о шаблоне Model View Presenter, о том, как он применяется в Android и каковы его наиболее важные преимущества. В этом руководстве мы более подробно рассмотрим шаблон Presenter Model Viewer, реализовав его в приложении для Android.

В этом уроке:

  • мы строим простое приложение, используя шаблон MVP
  • мы исследуем, как реализовать шаблон MVP на Android
  • и мы обсуждаем, как преодолеть некоторые трудности, вызванные архитектурой Android

Шаблон Model View Presenter — это архитектурный шаблон, основанный на шаблоне Model View Controller (MVC), который увеличивает разделение задач и облегчает модульное тестирование . Он создает три слоя: Модель , Вид и Представитель , каждый из которых имеет четко определенную ответственность.

Слои представления модели

Модель содержит бизнес-логику приложения. Он контролирует, как данные создаются, хранятся и изменяются. Представление — это пассивный интерфейс, который отображает данные и направляет действия пользователя в Presenter. Ведущий выступает в качестве посредника. Он извлекает данные из модели и показывает их в представлении. Он также обрабатывает действия пользователя, отправленные представлением.

Мы собираемся создать простое приложение для заметок, чтобы проиллюстрировать MVP. Приложение позволяет пользователю делать заметки, сохранять их в локальной базе данных и удалять заметки. Для простоты в приложении будет только одно действие.

Макет приложения

В этом уроке мы сконцентрируемся в первую очередь на реализации шаблона MVP. Другие функции, такие как настройка базы данных SQLite, создание DAO или обработка взаимодействия с пользователем, пропускаются. Если вам нужна помощь по какой-либо из этих тем, Envato Tuts + предлагает несколько отличных руководств по этим темам.

Начнем с создания новой заметки. Если мы разделим это действие на более мелкие операции, то вот как будет выглядеть поток с использованием архитектурного шаблона MVP:

  • Пользователь вводит заметку и нажимает кнопку добавления заметки.
  • Presenter создает объект Note с текстом, введенным пользователем, и просит модель вставить его в базу данных.
  • Модель вставляет заметку в базу данных и сообщает докладчику, что список заметок изменился.
  • Докладчик очищает текстовое поле и просит представление обновить его список, чтобы показать вновь созданную заметку.
Схема действий MVP

Давайте теперь рассмотрим операции, необходимые для выполнения этого действия, и разделим их с помощью MVP. Чтобы различные объекты были слабо связаны , связь между уровнями осуществляется с помощью интерфейсов. Нам нужно четыре интерфейса:

  • RequiredViewOps : требуется Просмотр операций, доступных для Presenter
  • ProvidedPresenterOps : операции, предлагаемые для просмотра для связи с Presenter.
  • RequiredPresenterOps : необходимые операции Presenter, доступные для Model
  • ProvidedModelOps : операции, предлагаемые модели для связи с Presenter
Интерфейсы MVP

Теперь, когда у нас есть представление о том, как должны быть организованы различные методы, мы можем приступить к созданию нашего приложения. Мы упростим реализацию, сосредоточившись только на действии, чтобы добавить новую заметку. Исходные файлы этого руководства доступны на GitHub .

Мы используем только одно действие с макетом, который включает в себя:

  • EditText для новых заметок
  • Button для добавления заметки
  • RecyclerView для просмотра всех заметок
  • два элемента TextView и Button внутри держателя RecyclerView

Давайте начнем с создания интерфейсов. Чтобы все было организовано, мы помещаем интерфейсы в держатель. Опять же, в этом примере мы сосредоточимся на действии, чтобы добавить новую заметку.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public interface MVP_Main {
    /**
     * Required View methods available to Presenter.
     * A passive layer, responsible to show data
     * and receive user interactions
     */
    interface RequiredViewOps {
        // View operations permitted to Presenter
        Context getAppContext();
            Context getActivityContext();
        void notifyItemInserted(int layoutPosition);
            void notifyItemRangeChanged(int positionStart, int itemCount);
    }
 
    /**
     * Operations offered to View to communicate with Presenter.
     * Processes user interactions, sends data requests to Model, etc.
     */
    interface ProvidedPresenterOps {
        // Presenter operations permitted to View
        void clickNewNote(EditText editText);
            // setting up recycler adapter
            int getNotesCount();
            NotesViewHolder createViewHolder(ViewGroup parent, int viewType);
            void bindViewHolder(NotesViewHolder holder, int position);
    }
 
    /**
     * Required Presenter methods available to Model.
     */
    interface RequiredPresenterOps {
        // Presenter operations permitted to Model
        Context getAppContext();
            Context getActivityContext();
    }
 
    /**
     * Operations offered to Model to communicate with Presenter
     * Handles all data business logic.
     */
    interface ProvidedModelOps {
            // Model operations permitted to Presenter
            int getNotesCount();
            Note getNote(int position);
        int insertNote(Note note);
            boolean loadData();
    }
}

Теперь пришло время создать слои Model, View и Presenter. Поскольку MainActivity будет действовать как представление, оно должно реализовывать интерфейс RequiredViewOps .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class MainActivity
        extends AppCompatActivity
    implements View.OnClickListener, MVP_Main.RequiredViewOps {
 
    private MVP_Main.ProvidedPresenterOps mPresenter;
    private EditText mTextNewNote;
    private ListNotes mListAdapter;
 
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.fab:{
                // Adds a new note
                mPresenter.clickNewNote(mTextNewNote);
            }
        }
    }
    @Override
    public Context getActivityContext() {
        return this;
    }
 
    @Override
    public Context getAppContext() {
        return getApplicationContext();
    }
    // Notify the RecyclerAdapter that a new item was inserted
    @Override
    public void notifyItemInserted(int adapterPos) {
        mListAdapter.notifyItemInserted(adapterPos);
    }
    // notify the RecyclerAdapter that items has changed
    @Override
    public void notifyItemRangeChanged(int positionStart, int itemCount){
        mListAdapter.notifyItemRangeChanged(positionStart, itemCount);
    }
    // notify the RecyclerAdapter that data set has changed
    @Override
    public void notifyDataSetChanged() {
        mListAdapter.notifyDataSetChanged();
    }
    // Recycler adapter
    // This class could have their own Presenter, but for the sake of
    // simplicity, will use only one Presenter.
    // The adapter is passive and all the processing occurs
    // in the Presenter layer.
    private class ListNotes extends RecyclerView.Adapter<NotesViewHolder>
    {
        @Override
        public int getItemCount() {
            return mPresenter.getNotesCount();
        }
 
        @Override
        public NotesViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
            return mPresenter.createViewHolder(parent, viewType);
        }
 
        @Override
        public void onBindViewHolder(NotesViewHolder holder, int position) {
            mPresenter.bindViewHolder(holder, position);
        }
    }
}

Ведущий является посредником и должен реализовать два интерфейса:

  • ProvidedPresenterOps чтобы разрешить вызовы из представления
  • RequiredPresenterOps для получения результатов от модели

Обратите особое внимание на ссылку на слой View. Нам нужно использовать WeakReference<MVP_Main.RequiredViewOps> поскольку MainActivity может быть уничтожена в любое время, и мы хотим избежать утечек памяти. Кроме того, слой Model еще не настроен. Мы сделаем это позже, когда соединим слои MVP.

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
public class MainPresenter implements MVP_Main.ProvidedPresenterOps, MVP_Main.RequiredPresenterOps {
 
    // View reference.
    // because the Activity could be destroyed at any time
    // and we don’t want to create a memory leak
    private WeakReference<MVP_Main.RequiredViewOps> mView;
    // Model reference
    private MVP_Main.ProvidedModelOps mModel;
 
    /**
     * Presenter Constructor
     * @param view MainActivity
     */
    public MainPresenter(MVP_Main.RequiredViewOps view) {
        mView = new WeakReference<>(view);
    }
 
    /**
     * Return the View reference.
     * Throw an exception if the View is unavailable.
     */
    private MVP_Main.RequiredViewOps getView() throws NullPointerException{
        if ( mView != null )
            return mView.get();
        else
            throw new NullPointerException(«View is unavailable»);
    }
 
    /**
     * Retrieves total Notes count from Model
     * @return Notes list size
     */
    @Override
    public int getNotesCount() {
        return mModel.getNotesCount();
    }
 
    /**
     * Creates the RecyclerView holder and setup its view
     * @param parent Recycler viewGroup
     * @param viewType Holder type
     * @return Recycler ViewHolder
     */
    @Override
    public NotesViewHolder createViewHolder(ViewGroup parent, int viewType) {
        NotesViewHolder viewHolder;
        LayoutInflater inflater = LayoutInflater.from(parent.getContext());
 
        View viewTaskRow = inflater.inflate(R.layout.holder_notes, parent, false);
        viewHolder = new NotesViewHolder(viewTaskRow);
 
        return viewHolder;
    }
 
    /**
     * Binds ViewHolder with RecyclerView
     * @param holder Holder to bind
     * @param position Position on Recycler adapter
     */
    @Override
    public void bindViewHolder(final NotesViewHolder holder, int position) {
        final Note note = mModel.getNote(position);
        holder.text.setText( note.getText() );
        holder.date.setText( note.getDate() );
        holder.btnDelete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                clickDeleteNote(note, holder.getAdapterPosition(), holder.getLayoutPosition());
            }
        });
 
    }
 
    /**
     * @return Application context
     */
    @Override
    public Context getAppContext() {
        try {
            return getView().getAppContext();
        } catch (NullPointerException e) {
            return null;
        }
    }
 
    /**
     * @return Activity context
     */
    @Override
    public Context getActivityContext() {
        try {
            return getView().getActivityContext();
        } catch (NullPointerException e) {
            return null;
        }
    }
 
    /**
     * Called by View when user clicks on new Note button.
     * Creates a Note with text typed by the user and asks
     * Model to insert it in DB.
     * @param editText EditText with text typed by user
     */
    @Override
    public void clickNewNote(final EditText editText) {
        getView().showProgress();
        final String noteText = editText.getText().toString();
        if ( !noteText.isEmpty() ) {
            new AsyncTask<Void, Void, Integer>() {
                @Override
                protected Integer doInBackground(Void… params) {
                    // Inserts note in Model, returning adapter position
                    return mModel.insertNote(makeNote(noteText));
                }
 
                @Override
                protected void onPostExecute(Integer adapterPosition) {
                    try {
                        if (adapterPosition > -1) {
                            // Note inserted
                            getView().clearEditText();
                            getView().notifyItemInserted(adapterPosition + 1);
                            getView().notifyItemRangeChanged(adapterPosition, mModel.getNotesCount());
                        } else {
                            // Informs about error
                            getView().hideProgress();
                            getView().showToast(makeToast(«Error creating note [» + noteText + «]»));
                        }
                    } catch (NullPointerException e) {
                        e.printStackTrace();
                    }
                }
            }.execute();
        } else {
            try {
                getView().showToast(makeToast(«Cannot add a blank note!»));
            } catch (NullPointerException e) {
                e.printStackTrace();
            }
        }
    }
 
    /**
     * Creates a Note object with given text
     * @param noteText String with Note text
     * @return A Note object
     */
    public Note makeNote(String noteText) {
        Note note = new Note();
        note.setText( noteText );
        note.setDate(getDate());
        return note;
 
    }
}

Уровень Model отвечает за обработку бизнес-логики. Он содержит ArrayList с примечаниями, добавленными в базу данных, ссылкой DAO для выполнения операций с базой данных и ссылкой на Presenter.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class MainModel implements MVP_Main.ProvidedModelOps {
 
    // Presenter reference
    private MVP_Main.RequiredPresenterOps mPresenter;
    private DAO mDAO;
    // Recycler data
    public ArrayList<Note> mNotes;
 
    /**
     * Main constructor, called by Activity during MVP setup
     * @param presenter Presenter instance
     */
    public MainModel(MVP_Main.RequiredPresenterOps presenter) {
        this.mPresenter = presenter;
        mDAO = new DAO( mPresenter.getAppContext() );
    }
 
     /**
     * Inserts a note on DB
     * @param note Note to insert
     * @return Note’s position on ArrayList
     */
    @Override
    public int insertNote(Note note) {
        Note insertedNote = mDAO.insertNote(note);
        if ( insertedNote != null ) {
            loadData();
            return getNotePosition(insertedNote);
        }
        return -1;
    }
 
     /**
     * Loads all Data, getting notes from DB
     * @return true with success
     */
    @Override
    public boolean loadData() {
        mNotes = mDAO.getAllNotes();
        return mNotes != null;
    }
     /**
     * Gets a specific note from notes list using its array position
     * @param position Array position
     * @return Note from list
     */
    @Override
    public Note getNote(int position) {
        return mNotes.get(position);
    }
      
    /**
     * Get ArrayList size
     * @return ArrayList size
     */
    @Override
    public int getNotesCount() {
        if ( mNotes != null )
            return mNotes.size();
        return 0;
    }
}

С слоями MVP нам нужно создать их экземпляры и вставить необходимые ссылки. Прежде чем мы это сделаем, нам нужно решить несколько вопросов, которые напрямую связаны с Android.

Поскольку Android не позволяет создавать экземпляры Activity , для нас будет создан экземпляр уровня View. Мы несем ответственность за создание экземпляров слоев Presenter и Model. К сожалению, создание этих слоев вне Activity может быть проблематичным.

Для этого рекомендуется использовать форму внедрения зависимости. Поскольку наша цель — сконцентрироваться на реализации MVP, мы выберем более простой подход. Это не лучший доступный подход, но его легче всего понять. Мы обсудим MVP и внедрение зависимостей позже в этой серии.

  • создать экземпляр Presenter и Model в Activity, используя локальные переменные
  • настроить RequiredViewOps и ProvidedModelOps в Presenter
  • настроить RequiredPresenterOps в модели
  • сохранить ProvidedPresenterOps как ссылку для использования в представлении
01
02
03
04
05
06
07
08
09
10
11
12
13
/**
* Setup Model View Presenter pattern
*/
private void setupMVP() {
     // Create the Presenter
     MainPresenter presenter = new MainPresenter(this);
     // Create the Model
     MainModel model = new MainModel(presenter);
     // Set Presenter model
     presenter.setModel(model);
     // Set the Presenter as a interface
     mPresenter = presenter;
}

Еще одна вещь, которую мы должны рассмотреть, это жизненный цикл Деятельности. Activity Android может быть уничтожена в любое время, а слои Presenter и Model также могут быть уничтожены вместе с ней. Мы должны исправить это, используя какой-то конечный автомат для сохранения состояния во время изменений конфигурации. Мы также должны сообщить другим слоям о состоянии Деятельности.

Для этого мы будем использовать отдельный класс StateMaintainer , который содержит фрагмент, который поддерживает его состояние и использует этот фрагмент для сохранения и извлечения наших объектов. Вы можете взглянуть на реализацию этого класса в исходных файлах этого урока .

Нам нужно добавить метод onDestroy в Presenter и Model, чтобы проинформировать их о текущем состоянии Деятельности. Нам также нужно добавить метод setView в Presenter, который будет отвечать за получение новой ссылки View для воссозданного Activity.

Посмотреть жизненный цикл
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MainActivity
        extends AppCompatActivity
    implements View.OnClickListener, MVP_Main.RequiredViewOps
{
    // …
    private void setupMVP() {
        // Check if StateMaintainer has been created
        if (mStateMaintainer.firstTimeIn()) {
            // Create the Presenter
            MainPresenter presenter = new MainPresenter(this);
            // Create the Model
            MainModel model = new MainModel(presenter);
            // Set Presenter model
            presenter.setModel(model);
            // Add Presenter and Model to StateMaintainer
            mStateMaintainer.put(presenter);
            mStateMaintainer.put(model);
 
            // Set the Presenter as a interface
            // To limit the communication with it
            mPresenter = presenter;
 
        }
        // get the Presenter from StateMaintainer
        else {
            // Get the Presenter
            mPresenter = mStateMaintainer.get(MainPresenter.class.getName());
            // Updated the View in Presenter
            mPresenter.setView(this);
        }
    }
    // …
}

Шаблон MVP способен решить некоторые проблемы, вызванные архитектурой Android по умолчанию. Это делает ваш код простым в обслуживании и тестировании. Вначале принятие MVP может показаться сложным, но как только вы поймете логику, стоящую за ним, весь процесс будет простым.

Теперь вы можете создать свою собственную библиотеку MVP или использовать уже доступное решение, такое как Mosby или simple-mvp. Теперь вы должны лучше понять, что эти библиотеки делают за кулисами.

Мы почти в конце нашего путешествия MVP. В третьей и последней части этой серии мы добавим модульное тестирование к миксу и адаптируем наш код для использования внедрения зависимостей с помощью Dagger . Я надеюсь увидеть вас там.