JavaRush/Java блог/Random/MVP в Android для самых маленьких

MVP в Android для самых маленьких

Статья из группы Random
участников
Когда я начинал свой путь Android-разработчика, слова «Архитектура мобильного приложения» вызывали у меня глубокое недоумение, гугл и статьи на Хабре вгоняли в ещё большую депрессию - смотрю в книгу, вижу фигу. Думаю, если ты читаешь эту статью, то уже не раз изучал эту картинку и пытался понять, что происходит: MVP в Android для самых маленьких - 1Проблема понимания архитектурного подхода в мобильной разработке, на мой взгляд, кроется в абстрактности самой архитектуры. У каждого разработчика своё видение того, как правильно реализовать тот или иной паттерн. Более-менее приличные примеры реализации 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
Комментарии (16)
  • популярные
  • новые
  • старые
Для того, чтобы оставить комментарий Вы должны авторизоваться
Kai Anonyma
Уровень 22
26 августа 2019, 06:20
Передавать ссылку на контекст, Активити в данном случае, первый шаг к утечке памяти. Если автор об этом знал, то почему не написал.
Oleh
Уровень 1
21 апреля 2019, 12:24
Дублировать все коллбэки в Presenter это нелогично, он вообще не должен знать ничего о андроид или ЖЦ или android.util.Log. Передавать View в конструкторе неблагодарное занятие - Presenter надо держать в памяти независимо от ЖЦ и не инстанциировать его каждый раз при поворотах.
Serj Tarasov
Уровень 17
16 сентября 2018, 00:18
Спасибо за пример, вполне доходчиво для начального ознакомления! Считаю, что опущена одна важная часть, а хотелось бы все таки видеть, как она должна реализовываться, а именно асинхронность при выполнении метода loadMessage(). В каком месте разумнее запускать новый поток (т.е. реализовывать асинхронность), в presenter'е или в repository? Если в repository, то loadMessage должен будет что то возвращать? А если в prenesnter, то почему? Должен ли он знать о том, что работа repository может быть асинхронной и не будет ли это лишней нагрузкой кодом на класс презентера?
3 сентября 2018, 13:41
Большое спасибо за статью! :)
Gregory Buiko
Уровень 1
24 августа 2018, 13:13
"Presenter живёт до тех пор пока живёт его View" Presenter можно запихнуть во ViewModel.
Paul Soia Android Developer в Tallium
25 августа 2018, 12:55
а где в mvp viewmodel?
Gregory Buiko
Уровень 1
31 августа 2018, 00:13
Наверное не очень точно сформулировал мысль. Имелась в виду ViewModel из Architecture components, этот объект не уничтожается при activity changes. Пример описан здесь: https://hackernoon.com/mvp-android-architecture-components-aef55e15bfe3
Alexey Savchenko
Уровень 0
4 июля 2018, 13:14
Привет. Прочитал твою статью всё понял всё дошло - отличная статья для толчка в MVP ! Но лично я сразу же столкнулся с небольшой проблемой - это запросы в сеть или DB. Было бы намного понятнее, если ты бы сделал сноску в классе Repository на то , что при общение с фоновыми потоками нужно вызывать OnFinishedListener! Это то чего не хватило мне! ИМХО.
19 июля 2018, 23:21
Привет! Работа с бэкграунд потоками и вообще асинхронный андроид – тема для отдельного материала. Возможно, дойдут руки написать и про это. Спасибо за отзыв! Рад, что материал полезен =)
Paul Soia Android Developer в Tallium
30 июня 2018, 15:48
public interface MainContract {
    interface View {
        void showText();
    }
Здесь в методе void showText() в параметры надо стрингу добавить
Alexey Egorov Android Developer в СберТех
2 марта 2018, 20:24
Спасибо за статью! У вас небольшая ошибка: метод String loadString(), который вы в дальнейшем называете String loadMessage(). И для красоты - логи убрать, как уже было сказано выше. Картинка структуры MVP-архитектуры конечно приведена классная, до сих пор на неё смотрю и не понимаю её :D
6 марта 2018, 01:43
Спасибо за замечание! Исправил. Логи будут интересны разве что новичкам для просмотра. Раз картинка до сих пор не понятна - материал стоит доработать =)
Alexey Egorov Android Developer в СберТех
7 марта 2018, 03:13
Про картинку - юмор, конечно же. После прочтения статьи всё становится предельно понятно. Но, увидев эту картинку в первый раз, возникло непреодолимое желание нарисовать её более, как бы это сказать, попонятливее :)
Aliaksandr Kavalenka
Уровень 17
1 марта 2018, 22:11
ok в целом. Чтобы код совсем очистить от лишнего стоило убрать логи )). Ну на счет жестких рамок все-таки не соглашусь, как миниму тот факт ,что view не общается с model это жестко запрещено!!!
6 марта 2018, 01:44
Спасибо за интерес к статье! Под отсутствием жёстких рамок я не имел ввиду их полное отсутствие =)
Aliaksandr Kavalenka
Уровень 17
6 марта 2018, 16:03
Может быть, но на статью и материал думаю стоит смотреть с точки зрения новичка (профессионал врядли будет здесь читать статьи), а обширное количество материала и зачастую протеворечивого - вводит в ступор и заблуждение, личный опыт )). Поэтому стоит каждое слово рассматривать с точки зрения - а как оно зайдет читателю! ИМХО. Вот кто-то прочитает про отсутсвие рамок и склеит View с Model )))) , а ведь он доверился автору и понял по своему. Как-то так.