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

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

Moxy — реализация MVP под Android с щепоткой магии

MVP – это способ разделения ответственности в коде приложения. Model предоставляет данные для Presenter. View выполняет две функции: реагирует на команды от пользователя(или от элементов UI), передавая эти события в Presenter и изменяет gui по требованию Presenter. Presenter выступает как связующее звено между View и Model. Presenter получает события из View, обрабатывает их(используя или не используя Model), и командует View о том, как она должна себя изменить.

  1. Сильно упрощается написание тестов к коду
  2. Легко менять какую-то часть, не ломая при этом другую
  3. Код разбивается на мелкие кусочки, за счёт чего он становится более понятным и читабельным
  1. Кода становится больше
  2. К этому подходу нужно привыкать
  3. На данный момент не сильно распространённый(но известный) подход, поэтому приходится всем рассказывать о нём

MVP в Android

  • Полное управление GUI
  • Обработка взаимодействия с пользователем.
  • Запуск асинхронных задач.
  • Обработка результата асинхронной задачи.

MVP снимает часть ответственности с Activity. Вся работа с асинхронными задачами уходит в Presenter. Вся бизнес-логика – в Presenter и Model. Activity, в свою очередь, становится View. Она начинает просто отображать то, что скажет Presenter и передаёт события в Presenter, чтобы тот решал, как быть дальше.

  1. View должна привязываться к уже имеющемуся Presenter при смене конфигурации
  2. После привязывания View к уже имеющемуся Presenter, View должна отображать актуальное состояние Presenter
  3. Presenter должен уметь(при необходимости) жить независимо от того, кто на него подписан или от него отписался

Moxy – теория

Наше решение сильно отличается от всех прочих(даже сама концепция MVP была модернизирована) тем, что между View и Presenter затесался ViewState. Причём он там абсолютно необходим. Он отвечает за то, чтобы каждая View всегда выглядела именно так, как того хочет Presenter. ViewState хранит в себе список команд, которые были переданы из Presenter во View. И когда „новая“ View присоединяется к Presenter, ViewState автоматически применяет к ней все команды, которые Presenter выдавал раньше. Таким образом получается, что не зависимо от того, что произойдёт со View по вине Android, View останется всё-равно в правильном состоянии. Для этого вам нужно будет только привыкнуть изменять View исключительно командами из Presenter. Заметим, что это одно из основных правил MVP и распространяется не только на Moxy.

Схематичная иллюстрация того, как это работает:

  1. Во View происходит событие , которое передаётся в Presenter
  2. Presenter передаёт команду во ViewState
  3. Presenter стартует асинхронный запрос в Model
  4. ViewState складывает команду в очередь команд, после чего передаёт её во View
  5. View приводит себя в состояние, указанное в команде
  6. Presenter получает результат запроса из Model
  7. Presenter передаёт во ViewState две команды и
  8. ViewState сохраняет команды и в очередь команд и передаёт их во View
  9. View приводит себя в состояние, указанное в командах и
  10. Новая/пересозданная View присоединяется к уже имеющемуся Presenter
  11. ViewState передаёт сохранённый список команд в новую/пересозданную View
  12. Новая/пересозданная View приводит себя в состояние, указанное в командах , и

Moxy – возможности

  • Presenter не пересоздаётся при пересоздании Activity(это в разы упрощает работу с многопоточьностью)
  • Автоматизация полного восстановления того, что видит пользователь при пересоздании Activity(в том числе при динамическом добавлении элементов Android View)
  • Возможность из одного Presenter менять сразу несколько View(на практике оказалось чрезвычайно удобно)
  • @InjectPresenter – аннотация для управления жизненным циклом Presenter
  • @InjectViewState – аннотация для привязывания ViewState к Presenter
  • @StateStrategyType – аннотация для управления стратегией добавления и удаления команды из очереди команд во ViewState
  • @GenerateViewState – аннотация для генерации кода ViewState для определенного интерфейса View
Moxy – MvpPresenter

Каждое приложение содержит в себе какую-то бизнес-логику. В концепции MVP, вся бизнес-логика располагается в Presenter и в Model. По факту это значит, что вы практически не программируете во View. Для того, чтоб ваш Presenter не превратился в God Object, нужно разделять каждый отдельный блок бизнес-логики в отдельный Presenter. В таком случае у вас получится много Presenter, но они будут очень простыми и понятными. Например, если у вас на одном экране было две бизнес-логики, а затем они разошлись на 2 разных экрана, то вы просто измените View. А Presenter какими были, такими и останутся. Так же, в этом случае вы сможете легко переиспользовать один Presenter в нескольких местах(например, BasketPresenter, сквозной через всё приложение). Ещё это упростит тестирование кода – вы просто проверите небольшой Presenter, что он всё делает правильно.

Для Presenter в Moxy заведен класс MvpPresenter<View extends MvpView> . В MvpPresenter содержится экземпляр ViewState, который в тоже время должен реализовывать тот самый тип View , который пришёл в MvpPresenter . Доступ к этому экземпляру ViewState можно получить из метода public View getViewState() . А во время разработки вы не думаете, что работаете со ViewState, а просто даёте через этот метод команды для View, как ей измениться. Так же есть методы для привязывания/отвязывания View от Presenter( public void attachView(View view) и public void detachView(View view) ). Обратите внимание на то, что к одному Presenter может быть привязано несколько View. Они будут всегда иметь актуальное состояние(за счёт ViewState). А если вы хотите, чтобы привязывание/отвязывание View проходило не через стандартное поле ViewState, то можете переопределить эти методы и работать с пришедшей View как хотите. Например, вы можете захотеть использовать нестандартный ViewState, который не реализует интерфейс View , если вам нужно.

В классе MvpPresenter так же есть интересный метод protected void onFirstViewAttach() . Очень важно понять, когда этот метод будет вызван и зачем он нужен. Этот метод вызывается тогда, когда к конкретному экземпляру Presenter первый раз будет привязана любая View. А когда к этому Presenter будет привязана другая View, к ней уже будет применено состояние из ViewState. И здесь уже не важно, эта новая View – совсем другая View, или пересозданная в результате смены конфигурации. Этот метод подходит для того, чтобы, например, загрузить список новостей при первом открытии экрана списка новостей.

В момент, когда во View пришла команда, вам может потребоваться понять, это новая команда, или это команда для восстановления состояния? Например, если это свежая команда, то нужно применить команду с анимацией. А иначе не надо применять анимацию. Можно это сделать через разные StateStrategy, или через сложные флаги в Bundle savedState . Но правильным решение будет использовать метод Presenter(или ViewState) public boolean isInRestoreState(View view) , который сообщит вам, в каком состоянии находится конкретная View. Таким образом вы сможете понять, нужна ли вам анимация, или нет.

Moxy – MvpView и MvpViewState

Самым простым компонентом MVP является View. Вам нужно завести интерфейс, который наследуется от интерфейса-маркера MvpView и описать в нём методы, которые будет уметь выполнять View. В дополнение ко View, наша библиотека имеет сущность ViewState, которая непосредственно связана со View. ViewState является наследником MvpViewState<View extends MvpView> . Он управляет одним, или несколькими, View(все одного типа View ). И каждый раз, когда во ViewState приходит команда из Presenter, ViewState отправляет её всем View, о которых он знает. Также у MvpViewState есть метод protected abstract void restoreState(View view) , который будет вызван когда какая-нибудь View будет пересоздана, или когда к Presenterко ViewState будет привязана новая View. Именно после того, как выполнится этот метод, „новая“ View примет нужное состояние.

Стоит заметить, что MvpViewState хранит в себе список всех привязанных к нему View. И будет хорошо, если вы не будете забывать отвязывать View, которые уже уничтожены. Но если вы вдруг забудете это сделать, сильно не переживайте – в MvpViewState хранятся не прямые ссылки на View, а WeakReference , что всё-таки поможет GC. А в случае, если вы используете такой механизм, как MvpDelegate, то можете не беспокоиться об этом – он как привязывает View к Presenter, так и отвязывает их.

Moxy – @GenerateViewState и @InjectViewState

Так как ViewState в большинстве случаев является довольно однообразной прослойкой между View и Presenter, был написан генератор кода, который сделает за вас всю грязную работу. Применяя аннотацию @GenerateViewState к вашему интерфейсу View, вы получите сгенерированный класс ViewState. И чтобы вам не пришлось в Presenter самостоятельно искать и создавать экземпляр этого класса, есть аннотация @InjectViewState. Достаточно просто применить её к классу вашего Presenter. Дальше MvpPresenter сам всё сделает – он создаст экземпляр этого ViewState, сложит его себе в качестве поля и будет везде использовать его. Вам же просто останется работать с методом public View getViewState() из MvpPresenter .

В том случае, если вы не хотите использовать @GenerateViewState , но ваш ViewState реализует интерфейс View, вы можете по прежнему использовать аннотацию @InjectViewState. В таком случае, передайте в эту аннотацию, в качестве параметра, класс вашего ViewState.

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

Поэтому такой код писать можно:

А такой код писать нельзя:

Moxy – StateStrategy для команд во ViewState

По умолчанию, все команды для View сохраняются во ViewState просто в том порядке, в котором они туда поступали. И после того, как команды были применены, они продолжают лежать в этой очереди. Но это поведение можно поменять, применяя аннотацию @StateStrategyType к интерфейсу View и к его методам. На вход эта аннотация получает параметр, в котором вы должны указать класс StateStrategy , который вы хотите использовать. Если применить эту аннотацию ко всему интерфейсу View, то те методы, для которых стратегия не указана, будут использовать эту стратегию.

StateStrategy управляет очередью команд через два метода: void beforeApply и void afterApply . Первый метод будет вызван перед тем, как команда будет отправлена во View(метод beforeApply будет вызван сразу, как только поступит какая-то команда из Presenter). В этом месте, в стратегии, указанной по умолчанию, и происходит добавление команды в очередь. Второй метод afterApply будет вызван каждый раз, когда команда будет применена ко View. И в первом, и во втором методе вы можете менять список команд как хотите.

  • AddToEndStrategy – добавит пришедшую команду в конец очереди. Используется по умолчанию
  • AddToEndSingleStrategy – добавит пришедшую команду в конец очереди команд. Причём, если команда такого типа уже есть в очереди, то уже существующая будет удалена
  • SingleStateStrategy – очистит всю очередь команд, после чего добавит себя в неё
  • SkipStrategy – команда не будет добавлена в очередь, и никак не изменит очередь

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

Moxy – MvpDelegate и жизненный цикл MvpPresenter

Сам по себе, Presenter нигде не создаётся, нигде не хранится и ниоткуда не достаётся. И чтобы вам не пришлось ничего придумывать для решения этих задач, мы сделали такой механизм, как MvpDelegate . Он следит за тем, чтобы там, где есть его экземпляр, были правильно инициализированы все Presenter. Для этого от вас требуется только передать в него все основные моменты жизненного цикла вашей View. Посмотреть какие методы когда вызывать, вы можете в классе MvpActivity или MvpFragment .

Для того, чтобы MvpDelegate нашел все Presenter, вы должны отметить их аннотацией @InjectPresenter. Эта аннотация очень мощная. Через неё вы можете управлять тем, сколько времени будет жить Presenter. Если вы хотите, чтобы Presenter жил только пока есть View, в которой он содержится(+ пока происходит смена конфигурации), то просто добавьте эту аннотацию к полю Presenter. В случае, если вы хотите, чтобы Presenter жил не зависимо от того, кто и когда на него подписан, вам нужно будет сделать две вещи. Первое – нужно сообщить MvpDelegate , что Presenter не привязан к жизненному циклу того, кто его запросил. Для этого, нужно выставить значение параметра type аннотации @InjectPresenter как PresenterType.GLOBAL. Второе – вы должны передать MvpDelegate информацию, по которой он сможет найти нужный вам Presenter в хранилище всех Presenter. Есть два варианта, как это сделать:

Первый вариант. В аннотации @InjectPresenter вы выставляете значение для параметра tag. Тогда MvpDelegate попытается найти в глобальном хранилище Presenter с таким тэгом. Если он его найдёт, то просто установит его в это поле. Иначе он создаст подходящий Presenter, сложит его в хранилище, и установит его в это поле. С учётом того, что к одному Presenter может быть привязано несколько View, этот механизм открывает очень много возможностей перед вами.

  1. Создайте свою реализацию PresenterFactory
  2. В аннотацию @InjectPresenter установите параметры:
    • В factory установите класс вашей PresenterFactory
    • В presenterId установите строковый идентификатор Presenter(это нужно для того, чтобы различать в одном классе Presenter с одинаковыми фабриками)
  3. Заведите свой интерфейс, содержащий один и только один метод, который будет возвращать параметр для factory нужного типа:
    • Аннотируйте этот интерфейс как @ParamsProvider(PresenterFactoryClass), передав аннотации, в качестве параметра, класс вашей PresenterFactory
    • Опишите метод, который будет возвращать параметр, должен на вход получать один параметр String (в этот параметр придёт тот самый параметр presenterId из аннотации @InjectPresenter)
  4. Объект, который содержит Presenter, в аннотации @InjectPresenter которого указанна эта PresenterFactory , обязан реализовывать созданный в п.п. 3. интерфейс

Кроме указанной выше функциональности, MvpDelegate умеет быть родительским/дочерним делегатом для другого. Это необходимо для того, чтобы вы могли автоматизировать жизненный цикл Presenter не только внутри Activity/Fragment, но и внутри других элементов, у которых нет самостоятельного жизненного цикла(например, в адаптере или даже в ViewHolder элемента адаптера). Если вы установите для одного MvpDelegate в качестве родительского другой MvpDelegate , то делегат-потомок будет получать все события жизненного цикла делегата-родителя. Для этого просто вызовите у целевого MvpDelegate метод public void setParentDelegate(MvpDelegate delegate, String childId) . В качестве delegate он ожидает получить родительский MvpDelegate . В качестве childId , вы должны указать уникальный идентификатор, по которому локальные Presenter одного делегата-потомка будут отличаться от локальных Presenter другого делегата-потомка.

Отметим, что если у родительского MvpDelegate уже был вызван метод onCreate . то вам необходимо самостоятельно вызвать метод onCreate у делегата-потомка. Почему это важно? Чтобы это понять, разберёмся, как работает MvpDelegate .

MvpDelegate кроме того, что управляет инициализацией полей Presenter, он делает ещё одну очень важную вещь. Он привязывает и отвязывает View от Presenter. Привязывание View к Presenter происходит в методе onStart , а отвязывание – в методе onDestroy . У Fragment немного по другому, см. на github.

После вызова onCreate у MvpDelegate , все поля, отмеченные аннотацией @InjectPresenter, готовы к работе. Но к ним ещё не привязана View. View будет привязан к Presenter после того, как будет вызван метод MvpDelegate void onStart() .

После этого Presenter может взаимодействовать со View(и тогда, если к этому Presenter впервые была привязана View, будет вызван метод Presenter void onFirstViewAttached() ). После вызова void onDestroy() у MvpDelegate , View будет отвязана от Presenter. И тут возникает два вопроса. Во-первых, почему View привязывается к Presenter не в onCreate , а в onStart ? Во-вторых, раз привязывание произошло в onStart , то почему отвязывание не в onStop , а в onDestroy ? Вполне резонные вопросы. А ответ на них заключается в том, что так а) удобней, и б) проще. Удобней это тем, что ViewState применяется ко View сразу, как только View была привязана к Presenter. И если выполнять привязывание View к Presenter в onCreate , то получается, что вам нужно будет в Activity самостоятельно вызвать метод делегата onCreate после того, как вы в onCreate Activity выполните всю инициализацию Android View. Это не удобно. Удобно просто сделать одну Activity, от которой будут наследоваться все Activity вашего приложения, и в методе onCreate этой Activity просто выполнить метод делегата onCreate . А с учётом того, что привязывание View происходит в onStart , никаких проблем не будет. Во-вторых, если делать отвязывание View в onStop , тогда привязывание точно будет происходит при каждом onStart (сейчас привязывание View происходит в onStart , только если до этого был выполнен onCreate ). А значит и ViewState будет восстановлен при каждом onStart . А значит всё состояние будет накатываться заново, даже если Activity View не было уничтожено, а просто становилось невидимым на время. Поэтому отвязывание View от Presenter происходит в onDestroy . Прим.: onDestroy не будет вызван, если Android решит убить процесс Activity, но в таком случае и Presenter будет уничтожен.

MvpDelegate использует специальное хранилище для Presenter. Доступ к этому хранилищу он получает через MvpFacade . MvpFacade – содержит в себе хранилище Presenter и некоторые другие элементы, призванные помочь MvpDelegate делать его работу оптимально. Не смотря на то, что MvpFacade является синглтоном, будет здорово, если вы выполните его метод public static void init() например, в методе onCreate() вашего Application. Или вы можете наследовать ваш Application от MvpApplication , поставляемого в Moxy. Тогда в момент, когда MvpDelegate обратится к этому синглтону, он уже будет готов к работе.

Moxy – Model

Важным элементом MVP является Model. Но в Moxy эта часть MVP никак не затронута. Всё дело в том, что в этом нет смысла. В каждом проекте свои требования к Model. Где-то Model это просто набор классов для работы с API и сама работа с API(например, через Retrofit). Где-то в Model входит ещё и дополнительная бизнес-логика. В каких-то проектах актуально использование подхода Clean Architecture. В таком случае внутри Model появляются дополнительные сущности, например, Interactor и Repository. А с учётом того, что Presenter полностью отвязан от жизненного цикла Activity, вы можете спокойно создавать экземпляр конкретной Model внутри Presenter и работать с ним. Используя DI вы можете подключать нужную Model в Presenter. А в будущем, используя тот же DI, спокойно подменять Model для тестов.

В любом случае, крайне удобно для работы с Model использовать Rx. Тогда вы можете сделать так, чтобы публичные методы Model возвращали Observable . В таком случае будет легко сделать взаимодействие ModelPresenter, и в то же время ModelModel. Это даст возможность легко сделать параллельное исполнение запросов из Presenter в Model.

Moxy – итого

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

Хотелось бы заметить, что библиотека никак не ограничивает вас в выборе реализации многопоточности в вашем приложении. Вы можете использовать Rx, AsyncTask, Thread, Executor. Главное, будьте аккуратны, работайте со View только с главного потока. Ещё, Moxy не решит проблем с commit() фрагментов после выполнения onSaveInstanceState() . Поэтому не забудьте закрывать транзакцию, используя commitAllowingStateLoss() . Так же, она не решит проблем с утечкой памяти – если вы передадите ссылку на Context/Activity/Fragment в Presenter(а потом ещё и во ViewState), то память может утечь. Будьте аккуратны.

Полезные материалы
Moxy – где брать

Чтобы подключить Moxy в свой проект, просто добавьте её в зависимостях. Moxy состоит из трёх частей. Одна из них отвечает за предоставление вам Moxy SDK. Её довольно просто подключить:

Если вы хотите иметь доступ к таким вспомогательным классам, как MvpApplication , MvpActivity и MvpFragment , так же подключите moxy-android :

Другая часть отвечает за обработку аннотаций и занимается генерацией кода. И здесь вам нужно определиться.

Если у вас нет никаких особых требований, ваш проект – обычный Android-проект, и вы не хотите, чтобы сгенерированный код был доступен из вашего когда, то подключите зависимость так:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

— новый чат Performance для обсуждения проблем производительности и для ваших пожеланий по содержанию курса по этой теме

Лекция 5 по архитектуре андроид приложений. Паттерн MVP

Пятая лекция курса по архитектуре клиент-серверных android-приложений, в которой мы поговорим о паттернах MVP, MVC и MVVM. Также научимся работать с библиотекой Mosby, которая реализует паттерн MVP в Android.

Введение

На прошлой лекции мы рассмотрели принципы Clean Architecture от Роберта Мартина, а также их адаптацию для разработки приложений под Android, и пришли к выводу, что эти принципы позволяют решить все озвученные задачи при построении архитектуры приложения: они делают код модульным, тестируемым и легко читаемым.

Но как говорится, нет предела совершенству, и такую архитектуру можно и нужно улучшать. Какие недостатки есть у архитектуры, предложенной ранее? Во-первых, даже для достаточно простых экранов мы получаем слишком много разных классов (View, Presenter, UseCase, Repository, Navigator и другие). Здесь нужно исходить из задачи – если вам нужно реализовать простой экран с парой текстов и кнопкой, не нужно реализовывать для него все эти принципы. Во-вторых, мы видели, что слой бизнес-логики практически не содержит кода. Да, конечно, мы писали достаточно простое приложение, в котором не может быть много логики, но разве это не самый распространенный случай на сегодняшний день? Сегодня существует правильная тенденция, когда вся бизнес-логика переносится на сторону сервера. Эта тенденция является правильной потому, что на стороне сервера логика описывается только один раз (а не для всех мобильных платформ по отдельности), а также потому, что на стороне сервера логику можно изменить гораздо более оперативно.

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

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

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

Поэтому итоговая схема может выглядеть следующим образом:

При этом мы не потеряем возможность тестирования и сохраним модульность архитектуры приложения. Более того, теперь у нас есть один основной элемент, который содержит всю логику конкретного экрана – это делегат (Controller / Presenter / ViewModel). И поэтому для упрощения почти всегда достаточно тестировать только его.

Поскольку работа делегата полностью зависит от того, какие данные он получает от сервера, для тестирования делегата достаточно подменять реализацию репозитория.[wpanchor >

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

Паттерн MVP

Мы уже рассмотрели суть паттерна MVP в общих чертах, теперь пора объяснить его более подробно, а также сравнить с другими паттернами, такими как MVC.

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

Основная идея любого из паттернов MVP, MVC, MVVM заключается в разделении логики и UI-части приложения так, чтобы их можно было тестировать по отдельности. При этом сами паттерны достаточно сильно отличаются между собой, поэтому рассмотрим их, чтобы понять, почему в Android в первую очередь используется MVP.

Сложно не заметить, что все эти паттерны содержат View и Model, а отличия заключаются в последнем элементе, который управляет логикой. В случае MVC это Controller, в случае MVP – Presenter, в MVVM – ViewModel. Для удобства можно назвать этот отличающийся объект делегатом. Разумеется, различия заключаются не только в названии, но и в том, как именно делегат управляет логикой и взаимодействует с View и Model.

Начнем рассмотрение паттернов с их общих частей – View и Model:

  • Есть два подхода к пониманию этой сущности. Кто-то оперирует понятием Model в смысле всего слоя данных в приложении: это и бизнес-объекты, содержащие логику, и способ их получения (Repository), и какие-то менеджеры и другие элементы, относящиеся к данным. Такой подход уместен, если говорить, что ваша система использует исключительно паттерн MVP и больше никаких элементов. Но мы решили сохранить слой данных в том виде, в котором он был изложен в принципах “чистой” архитектуры, поэтому под Model мы будем понимать обычные классы объектов, которые используются при взаимодействии View с делегатом. Плюс такого подхода заключается в том, что мы разделяем сущности, что может упрощать понимание. На конечный результат использование разных терминологий никак не влияет, но это нужно учитывать при изучении других источников.
  • View отображает данные, получаемые либо от Model, либо от делегата, что зависит от конкретного паттерна. View – эта та часть системы, которая видна пользователю и которая взаимодействует с ним. При этом View не должна содержать логику, а передавать результаты взаимодействия делегату, который будет управлять этой View.

Самым известным паттерном, конечно, является MVC, в котором делегатом является Controller. Схема этого паттерна выглядит следующим образом:

Когда пользователь взаимодействует со View (к примеру, нажимает на кнопку), View передает информацию об этом действии в Controller. Controller обрабатывает это событие в соответствии с логикой системы и изменяет Model. View отслеживает состояние модели, поэтому при изменении Model View получает уведомление и отображает новую информацию. Это активная модель, которая применяется чаще всего. Также существует пассивная модель, в которой View обновляется через Controller.

И есть еще один важный момент – Controller может управлять несколькими View, при этом Controller определяет, какая именно View будет отображаться в текущий момент. Именно из-за этой особенности паттерн MVC не слишком удобно применять в Android. В Android в роли View чаще всего выступает Activity, которую просто так сменить, разумеется, невозможно. Есть определенный вариант использования MVC в Android, когда разными View являются фрагменты, которые переключает Controller. В таком случае View может отслеживать изменение состояния через, к примеру, ContentProvider. Но, к сожалению, чаще всего попытки реализовать MVC в Android вели к использованию Activity в качестве God object (когда все сущности и вся логика находится в Activity).

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

Вероятно, паттерн MVP приобрел свою популярность в Android не в последнюю очередь связи с огромным количеством legacy кода, который нужно было отрефакторить. Так как в случае тривиального использования MVP это сделать просто – мы создаем экземпляр Presenter и интерфейс View и последовательно проходим по коду и переносим логику в Presenter. Это позволяет нам легко разнести по разным классам логику и UI, к чему мы и стремимся.

Давайте подведем итог всему, что было сказано ранее. Во-первых, наша архитектура включает слой данных. Слой данных включает в себя объект Repository, который выполняет работу с серверными запросами и кэшированием и занимается первичной обработкой ошибок, а также средства для замены Repository при тестировании. Во-вторых, основой архитектуры является паттерн MVP. Presenter – это объект, который управляет отображением данных через специальный интерфейс View. Presenter также обращается к репозиторию за получением данных и занимается обработкой жизненного цикла. View – это интерфейс, содержащий методы для работы с UI, который реализуется в Activity или Fragment. Model – обычные модельки сущностей.

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

  1. При открытии экрана нужно проверять текущее состояние авторизации, если пользователь уже авторизован, то открывать главный экран.
  2. Если поле ввода для логина или для пароля пустое, то при попытке нажать кнопку должна отобразиться ошибка под пустым полем.
  3. Если данные введены корректно, то при нажатии кнопки должен быть инициирован процесс авторизации (запрос на сервер).
  4. Во время выполнения запроса нужно отображать прогресс бар, который необходимо скрывать после окончания запроса при любом исходе.
  5. Если запрос выполнен успешно, то нужно открыть главный экран приложения.
  6. Если во время выполнения произошла ошибка, нужно показать ошибку под полем ввода для логина.

Даже при таком описании видно, что является логикой, а что относится к UI-части.

Создадим интерфейс для View. Какие методы нужны Presenter-у для управления View? Разумеется, это отображение и скрытие процесса загрузки, также это отображение ошибок и открытие главного экрана. Поэтому интерфейс View для экрана авторизации может выглядеть следующим образом:

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

В Android-приложениях View реализуется либо в Activity, либо во фрагменте. У нас простой экран, поэтому фрагменты не нужны. Реализуем интерфейс AuthView в Activity для авторизации:

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

Теперь создадим Presenter, который будет управлять логикой экрана, процессом авторизации и обработкой жизненного цикла (для обработки жизненного цикла будем использовать экземпляр LifecycleHandler из библиотеки RxLoader). Presenter-у, разумеется, также передается экземпляр AuthView, которым он будет управлять и специальный объект для управления жизненным циклом:

Далее, при старте экрана Presenter будет проверять, авторизован ли пользователь или нет, и, если авторизован, то открывать главный экран:

Теперь переходим к основному сценарию авторизации. Добавим следующий метод в Presenter:

Как мы и описывали раньше, Presenter проверяет логин и пароль на соответствии каким-то локальным условиям (в данном случае только на то, что они не пустые) и, либо показывает ошибку, либо выполняет запрос на сервер через репозиторий. Во время запроса он командует View отображать процесс загрузки, а после окончания – скрыть его. Он также обрабатывает результат и командует View либо показать ошибку, либо открыть главный экран.

Presenter управляет View, но при этом View должна уметь обращаться к Presenter-у для передачи управления в случае каких-то действий пользователя. Поэтому Presenter объявляется полем в классе, реализующем интерфейс View (то есть в Activity или фрагменте):

После этого Presenter инициализируется и дальше с ним можно работать при возникновении определенных событий. К примеру, при старте Activity мы вызываем метод init:

Или же, когда пользователь нажмет на кнопку входа, мы делегируем этот вызов Presenter-у:

Таким образом, мы грамотно разделили код экранов на части, отвечающие за логику и за UI, что упрощает написание кода и его понимание. При этом мы не слишком сильно отошли от рассмотренных принципов Clean Architecture, но теперь у нас меньше сущностей, и в такой системе проще разобраться.

Но нельзя забывать о второй главной цели такого разбиения – тестирование. Сейчас мы писали код в Presenter, мало задумываясь о том, насколько он будет удобен для тестирования. В случае тестов на JUnit мы сталкиваемся с определенными ограничениями, которые придется обходить. Для этого нужно будет перерабатывать Presenter. Но этим мы займемся в рамках следующей лекции.

После паттерна MVC мы сразу перешли к MVP и ничего не сказали о MVVM. Так сделано потому, что этому паттерну и библиотеке DataBinding будет посвящена отдельная лекция, в которой он будет разобран детально.

Перед тем, как переходить к рассмотрению тестирования, нужно рассмотреть еще несколько важных вопросов:

  1. Насколько такая архитектура масштабируема? В случае Clean Architecture у нас был отдельный UseCase на каждый серверный запрос, в нашем варианте такого нет. Понятно, что это не проблема, если в приложении всего 2-3 запроса, а если их 30? В таком случае мы не можем добавлять их все в один репозиторий. Во-первых, нужно будет выполнить разбиение слоя данных на классы для получения данных, для их кэширования и для обработки ошибок. Во-вторых, почти всегда все методы можно разбить на группы по сценариям приложения, и тогда у нас будет несколько репозиториев. При этом делегат никогда не должен использовать одновременно несколько репозиториев, так как это нарушает его контракт. Если у вас есть очень сложный экран (очень сложная Activity), то на нем можно определить несколько View и несколько делегатов.
  2. Может ли Presenter содержать классы Android, к примеру, Context? В идеале, разумеется, нет, так как это усложняет тестирование. Но в каких-то случаях это допустимо, если эти классы можно легко замокать на время тестирования. Иногда бывают ситуации, что для того, чтобы избежать передачи классов Android в Presenter нужно создать немало разных интерфейсов, и в итоге в Presenter попадает очень много объектов, что может усложнить тестирование еще больше. Поэтому общая рекомендация – старайтесь не использовать классы Android в Presenter, но не нужно слишком бояться этого.
  3. Нужно ли делать базовый интерфейс или базовый класс для Presenter-а? Нет, ни того, ни другого делать не нужно, хоть это может быть и удобно для того, чтобы явно видеть методы Presenter. Интерфейс обычно создают для того, чтобы объект, использующий интерфейс, ничего не знал о реализации, и чтобы можно было легко подменить эту реализацию. В MVP в Android каждый Presenter жестко связан (должен быть связан) с конкретным экраном, и он не будет меняться. К тому же View всегда знает, какой Presenter она будет использовать, поэтому смысла в интерфейсе нет. Базовый класс не стоит делать потому, что появляется желание перенести в него как можно больше [wpanchor логики, а это чревато потерей гибкости разных Presenter. Если вам нужно использовать много общей логики в разных Presenter, используйте композицию.

Дополнительно – Mosby

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

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

Однако нужно понимать, что при использовании библиотеки такого типа вы берете на себя большую ответственность. Более того, вы нарушаете первый из принципов Clean Architecture, который говорит о том, что архитектура не должна зависеть от конкретной библиотеки. Здесь же вы используете библиотеку, от которой будет полностью зависеть ваша архитектура. Поэтому, прежде чем принять такое решение, вы должны изучить принципы работы библиотеки и быть уверенными в том, что они вас устраивают. И как минимум, вы должны хорошо знать и уметь реализовывать паттерн MVP самостоятельно. Но даже в таком случае, возможно, стоит предпочесть свою реализацию архитектуры, так как это дает вам гибкость и соответствие вашим конкретным задачам.

После такого предупреждения уже можно перейти к рассмотрению конкретных решений. Вероятно, самой популярной и наиболее используемой библиотекой для реализации паттерна MVP является библиотека Mosby. Есть и более современные решения, к примеру, Moxy, которая расширяет функции Mosby (и берет на себя гораздо больше ответственности), но в силу того, что она пока что не используется настолько широко, мы не будем ее рассматривать.

Так чем же нам может помочь библиотека Mosby? При использовании ее мы получаем следующие преимущества:

  • Структурирование кода в соответствии с паттернами MVP. Если вы используете библиотеку для реализации паттерна MVP, вам сложно будет не писать код в соответствии с этим паттерном.
  • Библиотека Mosby позволяет не хранить явно экземпляры View в Presenter и Presenter во View. Более того, она выполняет автоматическое связывание View и Presenter.
  • В библиотеке Mosby решены некоторые стандартные задачи, к примеру, это LCE-экраны (Loading-Content-Error). Такие экраны могут находиться в 3 состояниях: загрузка, отображение данных и показ ошибки. Очень большое количество экранов являются экранами такого типа. Библиотека Mosby снимает часть ответственность с разработчика при реализации таких экранов. И да, в этом снова может быть проблема при использовании библиотеки. Возможно, они покрывают большинство стандартных случаев, но вам может потребоваться что-то другое, что добавить будет сложнее, чем при использовании собственной архитектуры.

Что нужно сделать для того, чтобы включить Mosby в свой проект? Во-первых, разумеется, добавить зависимости:

Во-вторых, каждый интерфейс View нужно унаследовать от MvpView или же, например, от MvpLceView, если вам нужен LCE-экран:

Интерфейс MvpView – это всего лишь маркер, который используется в качестве верхней границы в обобщенных классах Mosby. Далее, аналогично нам нужно унаследовать Presenter от базового класса MvpBasePresenter (не забываем и про то, что говорилось про наследование Presenter-ов):

Теперь в Presenter мы можем получить View с помощью метода getView, а также проверить, что View связана с Presenter-ом.

Конечно, если запустить код сейчас, это не сработает. Так как Presenter-ом управляет реализация View, то есть Activity или фрагмент, нужно модифицировать и ее. Mosby берет эту ответственность на себя, нужно только наследоваться от Activity из библиотеки, к примеру, BaseMvpActivity:

Тогда нужно только реализовать метод createPresenter:

Теперь в Activity вы можете получить доступ к Presenter-у с помощью метода getPresenter. Такой подход может быть достаточно удобным, так как вам не нужно думать о том, как связать View и Presenter. Это простой пример, который показывает, как можно использовать основные функции библиотеки. Хотя нельзя сказать, что использование библиотеки ради удаления пары строчек кода – это хорошая идея.

Как было сказано, еще один из примеров использования Mosby – это LCE-экраны. Однако решение в библиотеке не слишком удобное, так как в базовой Activity уже содержатся View элементы, которые предназначены для управления данными и показа ошибок, что не слишком удобно и лишает гибкости.

Кроме того, Mosby позволяет сохранять состояние View и восстанавливать его с помощью специального интерфейса ViewState. К сожалению, это возможность можно использовать только вместе с фрагментами.[wpanchor >

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

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

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