Что такое презентер android
Перейти к содержимому

Что такое презентер android

  • автор:

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

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

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

Java-университет

  • View знает о Presenter;
  • Presenter знает о View и Model (Repository);
  • Model сама по себе;
  • Contract регулирует взаимодействия между ними.
 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: «Вот тебе данные, отрисуй»

Android MVP пример для начинающих. Без библиотек и интерфейсов.

В этом посте описывается несложный пример MVP, без использования запутывающих интерфейсов и сложных библиотек.

Что такое MVP

Сначала немного теории о MVP. Схематично это выглядит так:

MVP расшифровывается как Model-View-Presenter (модель-представление-презентер). Если рассматривать Activity, которое отображает какие-то данные с сервера, то View — это Activity, а Model — это ваши классы по работе с сервером. Напрямую View и Model не взаимодействуют. Для этого используется Presenter.

Если в Activity пользователь нажал кнопку Обновить, то Activity сообщает об этом презентеру. При этом Activity не просит презентер загрузить данные. Оно просто сообщает, что пользователь нажал кнопку Обновить. А презентер уже сам решает, что по нажатию этой кнопки надо делать. Он запрашивает данные у модели и передает их в Activity, чтобы отобразить на экране.

Если экран отображает данные из базы данных, то модель — это база данных. Презентер может подписаться на уведомления модели об обновлении. В случае, когда данные в БД изменятся, модель оповестит об этом презентер. Презентер получит эти изменения и передаст их в Activity.

Можно сказать, что презентер — это логика, вынесенная из Activity в отдельный класс. А Activity остается для отображения данных и взаимодействия с пользователем. Если вы решили сделать другое Activity для отображения данных, то вам уже не нужно будет переносить логику в новое Activity, можно будет использовать готовый Presenter. А если вы решили поменять логику, то вам не нужно будет лезть в Activity и там, среди кода, который отвечает за отображение данных и взаимодействие с пользователем, искать логику и менять ее. Вы меняете код в презентере.

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

Практика

Я создал небольшое приложение и залил на гитхаб.

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

Чтобы наглядно показать отличие MVP, я сделал этот экран в двух вариантах: Activity и MVP. Вы можете выбрать нужный вариант при запуске:

Оба этих режима внешне будут выглядеть и работать одинаково, но «под капотом» они разные.

Первый вариант реализован с помощью одного Activity — SingleActivity. В нем реализовано следующее:
— вывод информации на экран и обработка нажатий
— логика (что делать по нажатию на кнопки и что/когда показывать)
— работа с базой данных.

Такой вариант реализации считается тяжелым и неудобным. Слишком много всего возложено на один класс.

Второй вариант реализован с помощью MVP — mvp.

В этом варианте я просто разделил код из SingleActivity на три класса в соответствии с MVP:
— в UsersModel — работа с базой данных (Model)
— в UsersActivity — вывод информации на экран и обработка нажатий (View)
— в UsersPresenter — логика (Presenter)

Давайте немного пройдемся по ключевым моментам кода. Сначала рекомендую вам посмотреть код SingleActivity, чтобы понять основные механизмы работы приложения. А я дальше буду описывать, как это было разделено по разным классам.

UsersModel

Это Model (модель). В модели обычно реализована работа с данными, например: запрос данных с сервера, сохранение в БД, чтение файлов и т.п.

Здесь находятся все операции с базой данных. Этот класс имеет три public метода, которые вызываются презентером:
loadUsers — получение списка пользователей из БД
addUsers — добавление пользователя в БД
clearUsers — удаление всех пользователей из БД

Что происходит внутри этих методов — касается только модели. Презентер будет просто вызывать эти методы и его не должно интересовать, как именно они реализованы.

Методам на вход можно передавать колбэки, которые будут вызваны по окончанию операции. Асинхронность работы с БД реализована с помощью AsyncTask. В методы добавления и удаления добавлены секундные паузы для наглядности.

UserActivity

Это View (представление). Представление отвечает за отображение данных на экране и за обработку действий пользователя.

Здесь есть несколько public методов, вызываемых презентером:
getUserData — получение данных, введенных пользователем
showUsers — отображение списка пользователей
showToast — отображение Toast
showProgress/hideProgress — скрыть/показать прогресс-диалог

В представлении не должно быть никакой логики. Это только инструмент для отображения данных и взаимодействия с пользователем.

Действия пользователя передаются в презентер. Обратите внимание на обработчики для кнопок Add и Clear. По нажатию на них, представление сразу сообщает об этом презентеру. И презентер уже будет решать, что делать.

Повторюсь, т.к. очень важно понимать это правильно. По нажатию на кнопки, Activity не просит презентер добавить пользователя или удалить всех пользователей. Т.е. оно не указывает презентеру, что ему делать. Оно просто сообщает, что была нажата кнопка Add или Clear. А презентер принимает это к сведению и действует по своему усмотрению.

UsersPresenter

Это Presenter (презентер). Он является связующим звеном между представлением и моделью, которые не должны общаться напрямую.

От представления презентер получает данные о том, какие кнопки были нажаты пользователем, и решает, как отреагировать на эти нажатия. Если надо что-то отобразить, то презентер сообщает об этом представлению. А если нужно сохранить/получить данные, он использует модель.

Давайте по шагам рассмотрим взаимодействие представления, презентера и модели на нашем примере. Возьмем сценарий добавления новых данных в базу.

1) Пользователь вводит данные в поля ввода. Это никак не обрабатывается и ничего не происходит.
2) Пользователь жмет кнопку Add. Вот тут начинается движ.
3) Представление сообщает презентеру о том, что была нажата кнопка Add.
4) Презентер просит представление дать ему данные, которые были введены пользователем в поля ввода.
5) Презентер проверяет эти данные на корректность.
6) Если они некорректны, то презентер просит представление показать сообщение об этом.
7) Если данные корректны, то презентер просит представление показать прогресс-диалог и просит модель добавить данные в базу данных.
8) Модель асинхронно выполняет вставку данных и сообщает презентеру, что вставка завершена
9) Презентер просит представление убрать прогресс-диалог.
10) Презентер просит свежие данные у модели.
11) Модель возвращает данные презентеру.
12 Презентер просит представление показать новые данные.

Из этой схемы видно, что презентер рулит всем происходящим. Он раздает всем указания, решает, что делать и как реагировать на действия пользователя.

Обратите внимание на методы презентера: attachView и detachView. Первый дает презентеру представление для работы, а второй говорит, что представление надо отпустить. Эти методы вызывает само представление. Первый метод — после своего создания, а второй — перед своим уничтожением. Иначе, если презентер будет держать ссылку на представление после его официального уничтожения, то может возникнуть утечка памяти.

Метод viewIsReady вызывается представлением, чтобы сообщить о том, что представление готово к работе. Презентер запрашивает у модели данные и просит представление отобразить их.

Плюсы MVP

Кратко напишу преимущества MVP по сравнению с Activity.
— легче писать тесты
— в небольших классах искать что-либо и вносить изменения легче, чем в одном большом
— бывает так, что одно представление используется разными презентерами, или наоборот — один презентер используется для разных представлений. Если у вас все в одном Activity — вы не сможете так сделать.

Все плюсы вытекают из того, что вместо одного класса, мы используем несколько.

Что дальше?

Я создал этот пример, чтобы максимально просто показать реализацию MVP. Реальный рабочий пример будет содержать несколько важных дополнений. Кратко расскажу о них, чтобы вы представляли себе, куда двигаться дальше.

Интерфейсы.

Это то, что очень запутывает новичков в примерах MVP, но действительно является очень полезным инструментом.

Обратите внимание на взаимодействие презентера и представления в нашем примере. У представления есть несколько методов, которые вызывает презентер: getUserData, showUsers, showToast, showProgress, hideProgress. Вот эти методы — это все что должен знать презентер. Ему не нужны больше никакие знания о представлении. А в текущей реализации презентер знает, что его представление — это UsersActivity. Т.е. это целое Activity с кучей методов, которые презентеру знать незачем. Использование интерфейсов решает эту проблему.

Мы можем создать интерфейс UsersContractView

interface UsersContractView < UserData getUserData(); void showUsers(Listusers); void showToast(int resId); void showProgress(); void hideProgress(); >

Добавить этот интерфейс в UsersActivity

public class UsersActivity extends AppCompatActivity implements UsersContractView

Теперь в презентере можно убрать все упоминания о UsersActivity, и оставить только UsersContractView.

public class UsersPresenter < private UsersContractView view; public void attachView(UsersContractView view) < this.view = view; >. >

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

А если презентер завязан на конкретный класс, например, UsersActivity, то при замене представления, вам придется открыть презентер и поменять там UsersActivity на другой класс.

Асинхронные операции

Для реализации асинхронности я здесь использовал AsyncTask. Но не помню, когда последний раз использовал его в рабочем коде. Обычно используются различные библиотеки, которые удобнее в использовании, гибче и дают больше возможностей. Например — RxJava.

Создание объектов

В UsersActivity мы создаем презентер следующим образом:

DbHelper dbHelper = new DbHelper(this); UsersModel usersModel = new UsersModel(dbHelper); presenter = new UsersPresenter(usersModel);

Это не очень хорошая практика. Рекомендуется не создавать объекты внутри вашего класса, а получать их уже готовыми снаружи. Для реализации этого принципа существуют различные библиотеки. Самый распространенный пример — это библиотека Dagger 2.

Поворот экрана

В этом примере нет обработки поворота экрана. Если ваш презентер выполняет какую-то долгую операцию, и вы повернете экран, у вас просто создастся новый презентер, а результаты работы старого презентера могут быть потеряны.

Есть различные способы, как этого избежать. Один из них — не пересоздавать презентер, если представление пересоздается. При этом, презенетер отпускает старое представление (метод detachView), и получает новое представление (метод attahcView). В итоге, результаты работы долгой операции будут отображены уже в новом представлении.

Если тема MVP стала вам интересна, то посмотрите этот пример. Он посложнее и более приближен к реальному коду.

Присоединяйтесь к нам в Telegram:

— в канале StartAndroid публикуются ссылки на новые статьи с сайта startandroid.ru и интересные материалы с хабра, medium.com и т.п.

— в чатах решаем возникающие вопросы и проблемы по различным темам: Android, Compose, Kotlin, RxJava, Dagger, Тестирование, Performance

— ну и если просто хочется поговорить с коллегами по разработке, то есть чат Флудильня

Построение MVP архитектуры Android приложения: советы и технологии

mvp architecture Построение MVP архитектуры Android приложения: советы и технологии

Построение правильной архитектуры мобильного приложения имеет первостепенное значение для всего проекта. Уделив должное внимание качеству кода, масштабируемости системы, выбору API сервера и надежных технологий, можно избежать большинства проблем в дальнейшем.

Заложенная программистами архитектура напрямую влияет на то, понадобится ли постоянная поддержка и “ремонт” приложения или же все будет работать (и продолжать работать) как надо. Один из главных практических советов, которые мы ходим тут привести, заключается в эффективном планировании времени.

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

Прямо противоположный результат не заставит себя ждать: куча багов, недоработок, трат и “перезаливок”. Поэтому необходимо все правильно спланировать (включая оценку объема работ) еще на начальном этапе проекта.

В этой статье мы поделимся эффективными подходами к созданию архитектуры Android приложений, в частности, принципами построения MVP (Model-View-Presenter) архитектуры.

Что такое MVP архитектура

Паттерн Model-View-Presenter является одним из шаблонов, используемых для проектирования архитектуры мобильного приложения. Model-View-Presenter имеет три слоя:

  • Model — представляет собой интерфейс, отвечающий за управление данными (включая кэширование данных и управление базами данных) приложения и “хранение” его бизнес-логики. В Android приложении роль Model часто выполняет REST API или база данных API.
  • Presenter — выступает посредником между Model и View и отвечает за обновление представления, реагируя на взаимодействие пользователей с обновлением модели. Вся логика представления находится в Presenter, который также контролирует Model и общается с View, что позволяет обновлять конкретный View, когда это нужно.
  • View — отвечает только за представление данных в виде, определяемым Presenter. Представление может быть реализовано с помощью любого Android виджета или всего, что может выполнять такие операции как показ ProgressBar, обновление TextView и заполнение RecyclerView.

Преимущества MVP архитектуры в Android приложении

MVP архитектура представляет собой эффективную архитектурную модель для разработки Android приложений. Основные преимущества паттерна Model-View-Presenter следующие:

  • Более простая отладка. Программистам проще делать отладку в приложении, так как MVP использует три разных слоя абстракций. Проводить модульное тестирование также проще, поскольку бизнес-логика полностью отделена от View.
  • Разрешает повторное использование кода. При построении MVP архитектуры разработчики могут многократно применять уже написанный код. Это становится возможным благодаря множеству представителей, которые контролируют Views. Такой подход более надежен по сравнению с использованием только одного presenter.
  • Эффективное разделение функциональностей приложения — под ним понимается отделение бизнес-логики и других частей от классов активности и фрагментов, которые, в свою очередь, лучше обеспечивают разделение функциональностей.

Практические советы по созданию MVP архитектуры

1. Улучшите способность Views к тестированию

Тестирование Views в Android является достаточно непростой задачей ввиду сложности фреймворка. Для улучшения процесса тестирования вам следует реализовать Passive View.

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

Для примера, если вам нужно реализовать фичу регистрации и отправления какой-либо заявки, включающую такие компоненты как форма с именем и паролем пользователи и кнопка “Отправить”/Отправить на рассмотрение” (“Send”/“Submit”), размещать логику валидации в presenter и только в нем, таким образом не усложняя view.

View, в свою очередь, должен содержать имя и пароль, которые он будет отправлять в presenter для проверки. Такой подход позволит значительно упростить процесс тестирования ваших Views.

2. Сделайте Presenter независимым от фреймворка

Следующий шаг в улучшении тестирования заключается в том, чтобы сделать Presenter независимым от фреймворка. Убедитесь, что он не зависит от Android классов. Если вы создаете ваше приложение на Java, то для написания Presenter используйте только ее зависимости.

Вы отделяете Presenter от деталей реализации (Android framework), благодаря чему можете писать тесты для Presenter без применения Robolectric (платформа для unit-тестирования Android приложений на JVM), а также быстрее запускать тесты на вашей локальной JVM без эмулятора.

3. Не создавайте обратные вызовы в интерфейсе Presenter

Presenter не должен содержать методы типа Action-lifecycle (onCreate (…), onStart (), onResume () и их двойные методы), так как они значительно усложняют его жизненный цикл активности.

Пользуйтесь случаем упростить жизненный цикл. Так, вместо вызова метода с тем же именем в обратном вызове жизненного цикла у вас есть возможность вызвать действие presenter. Например, (в конце Activity.onCreate (…) вы можете вызвать load () и т.д.).

4. Управляйте удаленными и локальными источниками данных в Model

Путем управления удаленными и локальными источниками данных в Model вы можете увеличить скорость работы вашего приложения. Например, вам нужно сделать так, чтобы приложение работало даже при отключении соединения с данными (в режиме офлайн).

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

Разместите логику, отвечающую за взаимодействие с presenter, в репозитории данных (data repository). Локальный класс хранения данных (local data storage class) может работать с кэшированными данными, тогда как класс удаленного источника данных (remote data source class) будет обрабатывать все удаленные вызовы API и ответы.

5. Используйте Dagger2 и RxJava для минимизации избыточности кода

Для того, чтобы свести избыточность кода к нулю и сделать Presenter независимым от жизненного цикла View, используйте такие библиотеки как Dagger2 и RxJava. Применение RxJava или ее аналогов позволит упростить базу кода.

В свою очередь, использование Dagger2 (библиотека внедрения зависимостей) упростит поведение компонентов путем введения зависимости между ними. Теперь рассмотрим внедрение зависимостей на примере его реализации при помощи Kotlin (о самом языке и его преимуществах мы поговорим позднее).

Сначала нужно добавить зависимости библиотеки Dagger2 в файл модуля build.gradle. Поскольку Dagger2 включает в себя обработчик аннотаций, следует применить плагин kotlin-apt к модулю.

apply plugin: ‘com.android.application’
apply plugin: ‘kotlin-android’
apply plugin: ‘kotlin-kapt’
apply plugin: ‘kotlin-android-extensions’

// …


dependencies
// …

// Dagger 2
implementation «com.google.dagger:dagger:2.14.1»
kapt «com.google.dagger:dagger-compiler:2.14.1»
compileOnly «org.glassfish:javax.annotation:3.1.1»
>

6. Используйте правильные соглашения об именовании методов

Правильные соглашения об именовании методов, которые вы используете в своей базе кода, позволяют эффективно распределить обязанности. Как правило, в Presenter у вас есть две разные категории методов — действия (activities) и пользовательские события (user events).

Пользовательские события представляют собой действия, которые запускаются пользователем. Действия — это методы, которые будут содержать логику, с которой будет обновляться представление (View).

Например, load () даст presenter команду загрузить другую страницу в то время как пользователь скроллит страницы приложения.

То есть при взаимодействии с Presenter View будет понимать, когда проводить самообновление, когда пользователь прокручивает страницу до конца. Таким образом, имена должны подходить к действиям, которые должны быть запущены.

7. Напишите контракт, описывающий взаимодействие между View и Presenter

Если вам нужно реализовать новую функцию, будет разумным начать с написания контракта, который детально описывает взаимодействие View и Presenter.

Хорошим практическим примером является решение, предлагаемое Google (в репозитории Android Architecture). Оно состоит из интерфейса с внутренними интерфейсами, один для view и второй для presenter:

public interface SearchRepositoriesContract
interface View
void addResults(List repos);
void clearResults();
void showContentLoading();
void hideContentLoading();
void showListLoading();
void hideListLoading();
void showContentError();
void hideContentError();
void showListError();
void showEmptyResultsView();
void hideEmptyResultsView();
>
interface Presenter extends BasePresenter
void load();
void loadMore();
void queryChanged(String query);
void repositoryClick(Repository repo);
>

Почему стоит использовать Kotlin для MVP архитектуры

Kotlin представляет собой статически типизированный язык программирования, который работает поверх JVM (Java Virtual Machine) полностью совместим с Java, что позволяет программистам использовать два языка в одном проекте. В прошлом году Kotlin был признан официальным языком для разработки Android приложений.

Kotlin создавался для решения нескольких задач, что вполне удалось. Во-первых, он наследует много преимуществ и характеристик Java, при этом убирая многие ее недостатки. Во-вторых, у него есть потенциал полностью заменить Java в будущем.

Несмотря на то что первая версия Kotlin была выпущена сравнительно недавно (в феврале 2016), на данный момент язык очень популярен, а его использование такими компаниями как Uber, Netflix, Coursera и Pinterest только подогревает к нему интерес.

По сравнению с Java, разработка мобильных приложений на Kotlin более простая. Код более читабельный и лаконичный, что заметно даже в самом простом примере:

Java code:

if (getActivity() instanceof LoginActivity)

((LoginActivity) getActivity ()). register ();

Kotlin code:

if (activity is LoginActivity)

activity. register ()

Одной из главных особенностей языка являются extension функции для стандартных Java коллекций. Благодаря ним уменьшается количество кода и снижается риск потенциальных ошибок, а работа с данными становится проще.

Также разработчикам легче осуществить последующую модификацию кода. Другая “полезность” Kotlin — его делегаты, которые способствуют использованию композиции на 100%.

Помимо указанных особенностей данного языка программирования, существуют и многие другие. Более того, JetBrains (российская компания, разработавшая Kotlin) постоянно работает над его улучшением, выпуская обновления и новые версии.

Подводя итоги, MVP архитектура имеет много преимуществ для разработки Android приложений. В статье мы постарались привести практические советы, которые помогут вам при ее построении. Надеемся, они будут вам полезны.)

Также спешим сообщить, что Smartum Pro расширяет команду разработчиков мобильных приложений и приглашает вас стать частью нашего дружного коллектива.

На данный момент у нас открыты вакансии Java и Android разработчиков, с которыми вы можете ознакомиться, перейдя по указанным ссылкам. Ждем ваше резюме!

  • Share on Twitter
  • Share on Facebook
  • Share on Google plus

Model-View-Presenter — компромисс и универсальный рецепт

image

Аббревиатуру MVP можно интерпретировать по разному, но в статье речь пойдет не о спорте.

В сети есть большое количество статей по архитектурным паттернам для iOS и Android разработчиков, и по MVP в частности. Иногда паттерн рассматривается в контексте обеих платформ. Кто-то выбирает данный паттерн для улучшения тестируемости своего кода, кто-то использует его в основном для разделения кода представления от модели. Также встречаются решения, которые используют MVP для унифицирования кода платформ, при условии что разработчики в компании владеют данными технологиями. Но общих слов и выводов иногда недостаточно для разработчика-прагматика. При проектировании коммерческих приложений неизбежно возникает множество деталей, которые общая архитектурная концепция не может раскрыть, и нельзя сказать, что есть единственное каноническое решение.

В статье я постараюсь описать ситуации, с которыми очень часто сталкиваются мобильные разработчики на реальных проектах, и когда действительно стоит задуматься о переходе на архитектурный паттерн более сложный чем “One UIViewController (Activity) to rule them all”. Или лучше сказать, когда нам это будет выгодно. Далее речь пойдет о компромиссе между временем и сложностью разработки в реалиях “обычных” проектов, которые в основном приходят на оценку и разработку.

Введение

Большинство мобильных проектов для нативных SDK сейчас являются клиент-серверными приложениями. В свою очередь такие приложения по сложности системы и времени жизни могут сильно отличаться. Есть приложения “однодневки” для работы в рамках одного мероприятия или ежегодного праздника, после которого они никому не нужны. Есть и серьезные проекты, требующие большого процента покрытия кода тестами, для обеспечения стабильной работы при внесении изменений разными участниками команды.

Какие проекты являются “обычными” можно определить по следующим критериям:

  • количество экранов: 5-20 штук, либо столько экранов должно быть в первой версии;
  • приложение должно быть спроектировано под две основные мобильные платформы: iOS и Android;
  • присутствует авторизация;
  • есть система кэширования с использованием как правило локальной базы данных;
  • делаются не только GET, но и POST, PUT, DELETE запросы, причем эти запросы вносят изменения в закэшированные данные, скачанные на предыдущем этапе;
  • приложение будет расширяться.
Сравнение MVP с паттернами по умолчанию

Рассмотрим самый простой паттерн, а именно iOS MVC с пассивной моделью, который является по сути жесткой связкой слоев представления и модели, где вся логика находится в UIViewController-е. Аналогичный подход получается и на Android-е, если вы пишите свои проекты по руководству из видеоуроков для начинающих или с большинства обучающих сайтов.

image

Если сравнить его с MVP, то основным отличием является класс Presenter, в который мы выносим логику обработки событий, форматирования данных и управления View. Под View мы будем понимать слой, включающий дочерние классы UIViewController, Activity или Fragment и их связку с классами всевозможных контролов. Таким образом мы “разгружаем” классы аналогичные UIViewController от тех обязанностей, которыми они не должны заниматься, оставляя внутри только код инициализации представлений и анимаций.

image

Как это часто бывает, в течение разработки первой версии приложения дополнительно добавляются функциональности, связанные с недостаточной проработкой ТЗ. Иногда изменения настолько радикальны, что приходится переписывать бОльшую часть уже проделанной работы. Наиболее часто встречается ситуация, когда просят добавить дополнительное поле к сущности и отобразить отформатированную информацию. Проследим какие классы мы изменим в проекте при добавление нового UI-элемента, отображающего отформатированную дату:

image

  • используя MVC с пассивной моделью:

image

  • используя MVP:

Очевидно, что мы усложнили систему введением паттерна MVP, так как количество классов, которые подверглись изменению при простом добавлении нового свойства сущности у нас возросло. Отдельно стоит отметить, что при реализации MVP создают отдельный интерфейс для View и пытаются сделать так, чтобы в Presenter-е находилось как можно меньше кода, связанного с платформой и не было прямых ссылок на конкретных наследников UIViewController, Activity или Fragment, потому что иначе будет соблазн написать код представления непосредственно в Presenter-е.

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

В поиске компромисса

Встает логичный вопрос, а зачем же все усложнять?

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

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

image

Не стоит путать модель представления в данной статье с ViewModel из MVVM, название лишь указывает на сущность, хранящую отформатированную и готовую к показу во View информацию.

При использовании обычного подхода мы, скорее всего, разместили бы код составления модели для ячейки в наследнике UIViewController-е или Activity (Fragment-е), что повлекло за собой увеличение кода в классе, в котором и так намешано немало, связанного с функционированием представления.

Написание “полезных” интеграционных тестов также становится возможным с вводом MVP, где основным объектом тестирования выступает Presenter. Конечно, используя только юнит-тестирование, можно протестировать используемые в приложении компоненты, которые являются God-объектами и наследниками UIViewController или Activity (Fragment), но это будет не вполне эффективно.

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

image

Небольшие уточнения к диаграмме:

    Для Android не актуально хранение ссылки на Presenter непосредственно в Activity, поэтому на практике у нас используется Retained Fragment и базовые классы для Activity и обычного Fragment-а, в которых описана логика инициализации и получения уже созданного ранее Presenter-а.

Много кода

public abstract class BasePresenterActivity extends AppCompatActivity < private static final String PRESENTER_FRAGMENT_TAG = "presenter_fragment_tag"; private P mPresenter; @Override protected void onCreate(@Nullable Bundle savedInstanceState) < super.onCreate(savedInstanceState); getPresenter().bindView(this); >@Override protected void onPause() < super.onPause(); if (isFinishing()) < PresenterFragment presenterFragment = (PresenterFragment) getSupportFragmentManager() .findFragmentByTag(PRESENTER_FRAGMENT_TAG); if (presenterFragment != null) < getSupportFragmentManager().beginTransaction().remove(presenterFragment).commit(); >> > @Override protected void onDestroy() < if (!isFinishing() && mPresenter != null) < mPresenter.unbindView(); >super.onDestroy(); > protected P getPresenter() < if (mPresenter != null) < return mPresenter; >PresenterFragment fragment = getPresenterFragment(); if (fragment.isPresenterSet()) < mPresenter = fragment.getPresenter(); >else < mPresenter = createPresenter(); fragment.setPresenter(mPresenter); >return mPresenter; > protected abstract P createPresenter(); private PresenterFragment getPresenterFragment() < PresenterFragment presenterFragment = (PresenterFragment) getSupportFragmentManager() .findFragmentByTag(PRESENTER_FRAGMENT_TAG); if (presenterFragment == null) < presenterFragment = new PresenterFragment(); getSupportFragmentManager() .beginTransaction() .add(presenterFragment, PRESENTER_FRAGMENT_TAG) .commit(); >return presenterFragment; > > 
public class PresenterFragment extends Fragment < private BasePresenter mPresenter; @Override public void onCreate(@Nullable Bundle savedInstanceState) < super.onCreate(savedInstanceState); setRetainInstance(true); >public P getPresenter() < return (P) mPresenter; >public void setPresenter(P presenter) < mPresenter = presenter; >public boolean isPresenterSet() < return mPresenter != null; >> 
public abstract class BasePresenter  < protected V mView; public void bindView(V view) < mView = view; >public void unbindView() < mView = null; >> 

Здесь выявляется единственный недостаток описываемой вариации MVP, а именно то, что сервис разрастается в пределах одного класса-файла. Если в iOS мы можем использовать категории и расширения для разделения одного большого класса-сервиса, то в Java придется разбивать сервис на классы подсервисы для работы с конкретным экраном.

Диаграмма разбиения сервиса на несколько подсервисов для Android

image

Если у вас не более 20-25 API методов, то код остается читаемым в рамках одного файла, при условии правильного форматирования и комментирования. Для первой версии продукта можно описывать всю логику получения и отправки данных в пределах одного класса. Но не стоит увлекаться, так как технический долг со временем будет увеличиваться.

Поскольку представление можно отделить от модели, то вам не составит труда показать заказчику свою работу, пусть и со статичными данными, в случае, если бэкенд еще не готов или работает нестабильно. Благо, есть множество способов замОкать серверную часть. Если бэкендом занимается независимая команда, то при наличии согласованной документации можно быть более менее уверенным в том, что при появлении настоящего сервиса для вас не окажется неожиданностью, какие поля шлются в API, и вы успеете отладить часть, связанную с форматированием данных для представления.

Без документации, я бы все таки остановил разработку на этапе создания дизайна экранов и выводом тестовых данных на основе моделей представления. В этом случае вы также получите более менее функциональный интерфейс пригодный для демо, но самое главное вы не потратите много времени на изменение сущностей для подгонки их под реальное API, так как Presenter выступит в роли адаптера, в котором будет осуществляться преобразование данных от сервиса в форматированные данные модели представления.

Универсальность

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

Следующим преимуществом использования паттерна MVP является разделение ресурсов разработчиков под разные платформы. Например, iOS синьор может написать часть кода под Android на уровне джуниора или мидла и быть при этом полезным, чтобы не пришлось за ним все переделывать. Сам я не сторонник “универсальных солдат”, так как лучше знать одну технологию хорошо, чем несколько на уровне джуниора или мидла. Практикующих старших разработчиков (не тимлидов или технических директоров) под обе платформы я не встречал, но не буду спорить, что такие профессионалы существуют. Основная причина дефицита таких людей, на мой взгляд, заключается в том, что в наше время очень тяжело уследить за развитием обеих технологий. Даже при условии того, что iOS и Android заимствуют некоторые конструкции друг у друга, под капотом реализация все равно разная. Но для небольших команд и “обычных” проектов подход с использованием непрофильного разработчика является вполне адекватным. Многие iOS программисты хотели бы попробовать себя в Android и наоборот. Можно отдать такому “легионеру” написать часть кода, связанного с Model или Presenter. Также будет неплохо, если человек сможет написать простой UI. Но задачи со сложными UI элементами с анимацией, оптимизацией, профилированием и многопоточностью, за исключением вызовов API сервиса, конечно, должен решать уже более опытный профильный разработчик.

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

Сравнение с другими известными архитектурными шаблонами

Есть еще один очень важный вопрос: почему за основу был выбран именно MVP, а не паттерны, основанные на чистой архитектуре или MVVM? В начале статьи мы постарались сконцентрироваться на том, что имеем дело с “обычными” проектами, которые желательно написать быстро, но с должным качеством и возможностью долгосрочной поддержки.

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

В MVVM мы должны перестроить логику мышления на декларативную. Также MVVM очень часто ассоциируется с библиотеками Rx*, с которыми должны быть знакомы программисты, а это сразу же ставит ограничение при поиске нового разработчика, либо вы осознанно тратите время на его обучение. В этом плане MVP прозрачнее и имеет меньше ограничений на умения разработчика.

Выводы

Перейдем к выводам. Преимущества, которые нам удалось выявить для паттерна MVP, если посмотреть на него со стороны выгоды внедрения в разработку:

  1. Настоящее отделение логики представления от модели, в отличие, к примеру, от Apple MVC;
  2. Четкое разделение ответственности между классами:
    • View — прорисовка, анимации, переходы между экранами;
    • Presenter — форматирование, реакция на события, логика представления и управление View;
    • Model — работа с загрузкой данных по API, извлечение данных из БД и их кэширование;
  3. Практически универсальная реализация интерфейсов Model-View-Presenter-а на мобильных платформах для “обычных” проектов;
  4. Интеграционное тестирование на уровне Presenter-а;
  5. Разделение задач в команде в рамках разработки одного экрана на задачи представления (вместе с Presenter-ом) и модели;
  6. Возможность показа интерактивного демо заказчику, без работающего бэкенда. При появлении реального сервиса мы не выбрасываем весь код, который написали, а адаптируем API;
  7. Возможность эффективного привлечения непрофильного разработчика из команды для написания кода на другой платформе;
  8. Расширение и добавление новых фич не вызовет эффекта “чужого кода”, когда человек возвращается к разработке проекта после длительного перерыва из-за набора простых правил размещения кода в строго определенных модулях.

Подводя итоги, можно утверждать следующее: если у вас есть небольшая команда из специалистов готовых на реализацию проектов под основные мобильные платформы, вам нужны универсальные архитектурные правила, а также возможность распараллеливания задач в рамках экрана и необходимость показа демо заказчику с частично работающей функциональностью, например, если практикуются спринты, то MVP определенно ваш выбор. Дополнительным бонусом выступает удобная поддержка проекта, которая не должна привести к фразе “а вот если бы мы сейчас написали все заново” и ситуациям, когда только для того, чтобы разобраться в коде одного огромного UIViewController-а или Activity будет потрачено больше времени, чем на саму фичу.

  • архитектура приложений
  • проектирование
  • mvp
  • разработка под ios
  • разработка под android
  • Разработка под iOS
  • Разработка мобильных приложений
  • Проектирование и рефакторинг
  • Разработка под Android

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *