Когда я начинал свой путь Android-разработчика, слова «Архитектура мобильного приложения» вызывали у меня глубокое недоумение, гугл и статьи на Хабре вгоняли в ещё большую депрессию - смотрю в книгу, вижу фигу. Думаю, если ты читаешь эту статью, то уже не раз изучал эту картинку и пытался понять, что происходит: Проблема понимания архитектурного подхода в мобильной разработке, на мой взгляд, кроется в абстрактности самой архитектуры. У каждого разработчика своё видение того, как правильно реализовать тот или иной паттерн. Более-менее приличные примеры реализации MVP нашлись в англоязычном секторе интернета, что не удивительно. Кратенько разберём, что есть что, и перейдём к примеру. Model — уровень данных. Не люблю использовать термин «бизнес логика», поэтому в своих приложениях я называю его Repository и он общается с базой данных и сетью. View — уровень отображения. Это будет Activity, Fragment или Custom View, если вы не любите плясок с бубном и взаимодействия с жизненным циклом. Напомню, что изначально все Android приложения подчинены структуре MVC, где Controller это Activity или Fragment. Presenter — прослойка между View и Model. View передаёт ему происходящие события, презентер обрабатывает их, при необходимости обращается к Model и возращает View данные на отрисовку. Применительно к Android и конкретному примеру, выделю важную часть - Contract. Это интерфейс, который описывает все взаимодействия между вышеперечисленными компонентами. Резюмируя теоретическую часть:
  • View знает о Presenter;
  • Presenter знает о View и Model (Repository);
  • Model сама по себе;
  • Contract регулирует взаимодействия между ними.
Собственно, сам пример, для простоты эксперимента будем по нажатию на кнопку загружать строку из БД и отображать в TextView. Например, БД содержит список лучших ресторанов города. Начнем с контракта: Создадим интерфейс MainContract:
public interface MainContract {
    interface View {
        void showText();
    }

    interface Presenter {
        void onButtonWasClicked();
        void onDestroy();
    }

    interface Repository {
        String loadMessage();
    }
}
Пока что мы просто выделяем 3 компонента нашего будущего приложения и что они будут делать. Далее опишем Repository:
public class MainRepository implements MainContract.Repository {

    private static final String TAG = "MainRepository";
    @Override
    public String loadMessage() {
        Log.d(TAG, "loadMessage()");
        /** Здесь обращаемся к БД или сети.
         * Я специально ничего не пишу, чтобы не загромождать пример
         * DBHelper'ами и прочими не относяшимеся к теме объектами.
         * Поэтому я буду возвращать строку Сосисочная =)
         */
        return "Сосисочная у Лёхи»;
    }
}
С ним всё понятно, просто загрузка - выгрузка данных. Далее на очереди Presenter:
public class MainPresenter implements MainContract.Presenter {
    private static final String TAG = "MainPresenter";

    //Компоненты MVP приложения
    private MainContract.View mView;
    private MainContract.Repository mRepository;

    //Сообщение
    private String message;


    //Обрати внимание на аргументы конструктора - мы передаем экземпляр View, а Repository просто создаём конструктором.
    public MainPresenter(MainContract.View mView) {
        this.mView = mView;
        this.mRepository = new MainRepository();
        Log.d(TAG, "Constructor");
    }

    //View сообщает, что кнопка была нажата
    @Override
    public void onButtonWasClicked() {
        message = mRepository.loadMessage();
        mView.showText(message);
        Log.d(TAG, "onButtonWasClicked()");
    }

    @Override
    public void onDestroy() {
        /**
         * Если бы мы работали например с RxJava, в этом классе стоило бы отписываться от подписок
         * Кроме того, при работе с другими методами асинхронного андроида,здесь мы боремся с утечкой контекста
         */

        Log.d(TAG, "onDestroy()");
    }
}
Помнишь, я писал про пляски с бубном и жизненный цикл? Presenter живёт до тех пор пока живёт его View, при разработки сложных пользовательских сценариев, советую дублировать все колбеки View в Presenter’e и вызывать их в соответствующие моменты, дублируя ЖЦ Activity/Fragment, чтобы вовремя понять что нужно сделать с теми данными, которые висят в данный момент в «прослойке». И наконец, View:
public class MainActivity extends AppCompatActivity implements MainContract.View {

    private static final String TAG = "MainActivity";

    private MainContract.Presenter mPresenter;

    private Button mButton;

    private TextView myTv;

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

        //Создаём Presenter и в аргументе передаём ему this - эта Activity расширяет интерфейс MainContract.View
        mPresenter = new MainPresenter(this);

        myTv = (TextView) findViewById(R.id.text_view);
        mButton = (Button) findViewById(R.id.button);

        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mPresenter.onButtonWasClicked();
            }
        });
        Log.d(TAG, "onCreate()");
    }

    @Override
    public void showText(String message) {
        myTv.setText(message);
        Log.d(TAG, "showMessage()");
    }

    //Вызываем у Presenter метод onDestroy, чтобы избежать утечек контекста и прочих неприятностей.
    @Override
    public void onDestroy() {
        super.onDestroy();
        mPresenter.onDestroy();
        Log.d(TAG, "onDestroy()");
    }
}
Что же происходит?
  • Activity, она же View, в методе onCreate() создаёт экзмпляр Presenter и передаёт ему в конструктор себя.
  • Presenter при создании явно получает View и создаёт экзмепляр Repository (его, кстати, можно сделать Singleton)
  • При нажатии на кнопку, View стучится презентеру и сообщает: «Кнопка была нажата».
  • Presenter обращается к Repository: «Загрузи мне вот эту шнягу».
  • Repository грузит и отдаёт «шнягу» Presenter’у.
  • Presenter обращается к View: «Вот тебе данные, отрисуй»
Вот и всё, ребята. P.S. Важно чётко разграничить обязанности между компонентами. К примеру, в одном из моих учебных проектов по нажатию на кнопку необходимо было изменить данные в БД. Модель описывалась POJO - классом, я передавал информацию о расположении view, отвечающей за информацию об объекте на экране, Presenter искал этот объект в списке и отдавал его на запись в Repository. Кажется, всё логично? Но мой наставник обратил внимание на следующее: Repository должен заниматься ТОЛЬКО записью и чтением, он не должен вытаскивать из POJO нужную информацию и принимать решение, что ему нужно писать писать. Presenter должен отдать ему только информацию на запись и ничего более. Нет жёстких рамок реализации архитектуры: экспериментируй, пробуй, ищи то, что удобно лично тебе. И не забудь показать старшим товарищам на code review =) Пример доступен на GitHub: https://github.com/admitrevskiy/MVP_Example