- Moxy android для чего
- Стратегии в Moxy (Часть 2)
- Зачем нужны кастомные стратегии
- Механика работы команд Moxy
- Создаем кастомную стратегию
- Предназначение..
- Moxy — реализация MVP под Android с щепоткой магии
- Что такое MVP
- MVP в Android
- Moxy – теория
- Moxy – возможности
- Moxy – MvpPresenter
- Moxy – MvpView и MvpViewState
- Moxy – @GenerateViewState и @InjectViewState
- Moxy – StateStrategy для команд во ViewState
- Moxy – MvpDelegate и жизненный цикл MvpPresenter
- Moxy – Model
- Moxy – итого
- Полезные материалы
- Moxy – где брать
Moxy android для чего
Буквально год назад коллеги по работе посоветовали мне 2 хорошие библиотеки, по их словам они «должны облегчить мою жизнь». Я посмотрел и поначалу отнесся к ним скептически, т.к. навигацию я уже более-менее сам писать умел, также был немного знаком с MVVM. И всё же, начался новый проект, и была поставлена задача протестировать на нем связку Moxy + Cicerone.
В этой статье я хочу рассказать, что из этого вышло, что мне понравилось и в каких ситуациях эта связка мне показалась неудобной.
Немного о Moxy
Moxy — это реализация паттерна MVP для Android. MVP — это способ разделения ответственности в приложениях. Я не буду описывать как происходит взаимодействие между компонентами MVP в теории, это можно посмотреть в Wikipedia, и более того, большая часть разработчиков уже это знают. Важно то, что в Android всё усложняется пересозданием Activity при изменении любого параметра конфигурации (локализация, ориентация и размер экрана, размер шрифта и другие), и если это изменение происходит во время работы приложения, то состояние Activity необходимо где-то хранить. Есть встроенные способы сохранения состояния через переопределение методов onSaveInstanceState и onRestoreInstanceState, но зачастую этот метод нарушает архитектуру приложения. Разработчики Moxy сделали всё, чтобы нам можно было не думать о сохранении состояния Activity или Fragment. Взглянем на схему:
Как видно, здесь не совсем обычный MVP, т.к. есть четвертый компонент: ViewState. Именно он отвечает за сохранение состояние View после пересоздания. Также ViewState позволяет одному Presenter обслуживать несколько View одновременно или по очереди. Как видно из схемы, новое View, которое присоединилось к Presenter позже первого, получило то же самое состояние, что и первая View. В некоторых случаях это очень удобно.
Если данная схема кажется вам очень сложной, то это только поначалу. На практике становится всё понятно. Взгляните на код:
Немного о Cicerone
Название никак не связано с древнеримским политическим деятелем. Скорее оно связано со значением старого итальянского слова «чи-че-ро-не«, что означало «гид для иностранцев«.
Если подходить к навигации с точки зрения паттерна, то это больше бизнес-логика. Но чтобы открыть Activity в Android, как минимум нужен Context, и передача его в сущность, занимающуюся бизнес-логикой (presenter, viewModel, и т.д.) — это анти-паттерн. А если вы используете архитектуру Single Activity, то для переключения фрагментов требуется учитывать жизненный цикл контейнера (отсылка к java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState у фрагментов).
Решение от Cicerone полностью устраняет эти проблемы. Чтобы понять, как это работает, взглянем на структуру:
Navigator — непосредственная реализация переключения экранов;
Command — команда перехода, которую выполняет Navigator;
CommandBuffer — менеджер, осуществляющий доставку команд навигатору, а также хранение, если навигатор по определенным причинам не может их выполнить в момент поступления;
NavigatorHolder — посредник между Navigator и CommandBuffer (не указан на схеме);
Router — объект, генерирующий низкоуровневые команды для Navigator посредством вызова высокоуровневых методов разработчиком для осуществления навигации;
Screen — высокоуровневая реализация конкретного экрана.
Изначально Cicerone имеет 4 базовые команды переходов:
- Back()— удаляет текущий экран из стека и делает активным предыдущий экран;
- BackTo(Screen) — возвращает на указанный экран, если он есть в цепочке, и удаляет все экраны, что были впереди. Если указанный экран не будет найден в стеке или вместо экрана передать null, то переход осуществится на корневой (root) экран;
- Forward(Screen) — добавляет экран в стек и делает его активным;
- Replace(Screen) — заменяет активный экран на указанный.
Из этих команд в классе Router сформированы шесть базовых высокоуровневых команд:
- navigateTo(Screen)— переход на новый экран;
- newScreenChain(Screen) — сброс стека до корневого экрана и открытие одного нового;
- newRootScreen(Screen) — сброс стека и замена корневого экрана;
- replaceScreen(Screen) — замена активного экрана;
- backTo(Screen) — возврат на любой экран в стеке;
- exit()— выход с активного экрана.
Оба этих списка могут быть расширены под конкретные нужды разработчика. В первом случае нужно реализовать интерфейс Command, во втором — расширить класс Router.
Чтобы добавить анимацию переходов между экранами, нужно либо полностью реализовать интерфейс Navigator, либо переопределить у базового экземпляра SupportAppNavigator метод setupFragmentTransaction, в котором в качестве параметра имеется экземпляр класса FragmentTransaction. Именно ему нужно добавить либо Transition, либо Animation.
Предлагаю посмотреть небольшой пример использования Cicerone в связке с Moxy:
Источник
Стратегии в Moxy (Часть 2)
В части 1 мы разобрались, для чего нужны стратегии в Moxy и в каких случаях уместно применять каждую из них. В этой статье мы рассмотрим механизм работы стратегий изнутри, поймем, в каких случаях нам могут понадобиться кастомные стратегии, и попробуем создать свою собственную.
Зачем нужны кастомные стратегии
Зачем вообще Moxy поддерживает создание
пользовательских стратегий? При проектировании библиотеки мы (I) старались учесть все возможные случаи, и встроенные стратегии практически на сто процентов их покрывают. Однако в некоторых случаях может потребоваться больше власти над ситуацией, и мы не хотели вас ограничивать. Рассмотрим один из таких случаев.
Презентер отвечает за выбор бизнес-ланча, который состоит из бургера и напитка.
Команды в зависимости от функции у нас делятся на следующие типы:
- кастомизируют бургер (добавить/удалить сыр, выбрать ржаную/пшеничную булку и т.д.);
- кастомизируют напиток (выбрать количество ложек сахара, добавить/удалить лимон и т.д);
- оповещают о том, что заказ отправлен.
Итак, мы хотим уметь отдельно управлять очередями команд для бургера и для напитка. Стратегий по умолчанию для этого не хватит, дополнительно нам нужны бургерные и напиточные! Давайте их изобретем, но для начала разберемся, как вообще устроен механизм применения команд и на что влияют стратегии.
Механика работы команд Moxy
Начнем издалека: в конструкторе презентера создается ViewState, все команды проксируются через него. ViewState содержит очередь команд — ViewCommands (класс, который отвечает за список команд и стратегий) и список View. В списке View может содержаться как несколько View, если вы используете презентер типа Global или Weak, так и ни одного (в ситуации, когда у вас фрагмент ушел в бэк стэк).
Между нами говоря, не стройте на их основе архитектуру, так как они залезают не в свою зону ответственности. Шарить данные между экранами лучше при помощи общих сущностей типа интеракторов в чистой архитектуре. О чистой архитектуре есть неплохая статья
Для начала разберемся, что же представляет собой стратегия:
Это интерфейс с двумя методами: beforeApply и afterApply. Каждый метод на вход принимает новую команду и текущий список команд, который и будет меняться (или останется без изменений) в теле метода. У каждой команды мы можем получить тэг (это строка, которую можно указать в аннотации StateStrategyType) и тип стратегии (см. листинг ниже). Как именно менять список решаем, опираясь только на эту информацию.
Давайте поймем, когда у нас будут вызываться данные методы. Итак, у нас есть интерфейс SimpleBurgerView, который умеет только добавлять немного сыра(II).
Рассмотрим, что происходит при вызове метода toggleCheese у сгенерированного класса LaunchView$$State (см. листинг):
1) Создается команда ToggleCheeseCommand (см. листинг ниже)
2) Вызывается метод beforeApply для класса ViewCommands для данной команды. В нем мы получаем стратегию и вызываем ее метод beforeApply. (см. листинг ниже)
Ура! Теперь мы знаем, когда выполняется метод beforeApply у стратегии: сразу же после соответствующего вызова метода у ViewState и только тогда. Продолжаем погружение!
В случае если у нас есть View:
3) Им поочередно проксируется метод toggleCheese.
4) Вызывается метод afterApply для класса ViewCommands для данной комнады. В нем мы получаем стратегию и вызываем ее метод afterApply.
Однако afterApply вызывается не только в этом случае. Он также будет вызван в случае аттача новой View. Давайте рассмотрим этот случай. При аттаче View вызывается метод attachView(View view)
Метод attachView(View view) вызывается из метода onAttach() класса MvpDelegate. Тот в свою очередь вызывается довольно часто: из методов onStart() и onResume(). Тем не менее библиотека гарантирует, что afterApply вызовется один раз для приаттаченой вью (см. листинг ниже).
1) Вью добавляется в список.
2) Если ее в этом списке не было, вью переводится в состояние StateRestoring
.
Это даёт возможность активити/вью/фрагменту понять, что состояние восстанавливается. У презентера есть метод isInRestoreState(). Этот механизм необходим для того, чтобы не выполнять некоторые действия дважды (например, старт анимации для перевода вью в нужное состояние). Это единственный метод presenter, который возвращает не void. Данный метод принадлежит presenter, т.к. view и mvpDelegate могут иметь несколько презентеров и помещение их в эти классы привело бы к коллизиям.
3) Далее происходит восстановление состояния
Стоит отметить, что метод afterApply может вызываться несколько раз. Обратите на это внимание, когда будете писать свои кастомные стратегии.
Мы познакомились с тем, как работают стратегии, пришло время закрепить навыки на практике.
Создаем кастомную стратегию
Схема работы
Итак, для начала давайте поймем, какую именно стратегию мы хотим получить. Договоримся об обозначениях.
Данная схема похожа на схему из первой части, однако в ней есть важное отличие -— появилось обозначение тэга. Отсутствие тэга у команды обозначает, что мы его не указывали и он принял значение по умолчанию — null
Мы хотим реализовать следующую стратегию:
При вызове презентером команды (2) со стратегией AddToEndSingleTagStrategy:
- Команда (2) добавляется в конец очереди ViewState.
- В случае, если в очереди уже находилась любая другая команда с аналогичным тэгом, она удаляется из очереди.
- Команда (2) применяется ко View, если оно находится в активном состоянии.
При пересоздании View:
- Ко View последовательно применяются команды из очереди ViewState
Реализация
1) Реализуем интерфейс StateStrategy. Для этого переопределяем методы beforeApply и afterApply
2) Реализация метода beforeApply будет очень похожа на реализацию аналогичного метода в классе AddToEndSingleStrategy.
Мы хотим удалить из очереди абсолютно все команды с данным тегом, т.е. удаляться будут даже команды с другой стратегией, но аналогичным тегом.
Поэтому мы вместо строчки entry.class == incomingCommand.class будем использовать entry.tag == incomingCommand.tag
В Kotlin == равносильно .equals в Java
Также нам необходимо убрать строку break, так как в отличие от AddToEndSingleStrategy у нас в очереди могут появиться несколько команд для удаления.
3) Реализацию метода afterApply оставим пустой, так как у нас нет необходимости менять очередь после применения команды.
Итак, что у нас получилось:
Вот и все, осталось проиллюстрировать, как мы будем использовать стратегию (см. листинг ниже).
Полный пример кода можно посмотреть в репозитории Moxy. Напоминаю, что это сэмпл и решения в нем приведены сугубо для иллюстрации функционала фреймворка
Предназначение..
Для чего еще можно использовать кастомные стратегии:
1) склеивать предыдущие команды в одну;
2) менять порядок выполнения команд, если команды не коммутативны (a•b != b•a);
3) выкидывать все команды, которые не содержат текущий tag;
4) ..
Если вы часто используете команды, а их нет в списке дефолтных — пишите, обсудим их добавление.
Обсудить Moxy можно в чате сообщества
Ждем замечаний и предложений по статье и библиотеке 😉
(I) здесь и далее «мы» — авторы Moxy: Xanderblinov, senneco и все ребята из сообщества, которые помогали советами, замечаниями и пулреквестами. Полный список контрибьютеров можно посмотреть здесь
(II) Код в листингах написан на Kotlin
Источник
Moxy — реализация MVP под Android с щепоткой магии
Что такое MVP
MVP – это способ разделения ответственности в коде приложения. Model предоставляет данные для Presenter. View выполняет две функции: реагирует на команды от пользователя(или от элементов UI), передавая эти события в Presenter и изменяет gui по требованию Presenter. Presenter выступает как связующее звено между View и Model. Presenter получает события из View, обрабатывает их(используя или не используя Model), и командует View о том, как она должна себя изменить.
У такого подхода к разделению ответственности есть ряд плюсов:
- Сильно упрощается написание тестов к коду
- Легко менять какую-то часть, не ломая при этом другую
- Код разбивается на мелкие кусочки, за счёт чего он становится более понятным и читабельным
В то же время, конечно, есть и минусы:
- Кода становится больше
- К этому подходу нужно привыкать
- На данный момент не сильно распространённый(но известный) подход, поэтому приходится всем рассказывать о нём
MVP в Android
Activity в Android является God object. На ней обычно лежит следующая ответственность:
- Полное управление GUI
- Обработка взаимодействия с пользователем.
- Запуск асинхронных задач.
- Обработка результата асинхронной задачи.
Самое печальное, наш God Object не бессмертен – Activity ещё и умирает при смене конфигурации.
MVP снимает часть ответственности с Activity. Вся работа с асинхронными задачами уходит в Presenter. Вся бизнес-логика – в Presenter и Model. Activity, в свою очередь, становится View. Она начинает просто отображать то, что скажет Presenter и передаёт события в Presenter, чтобы тот решал, как быть дальше.
Перед написанием своего решения мы изучили множество статей и реализаций концепции MVP в Android(см. ссылки в конце статьи). На основании анализа сложился список требований к решению:
- View должна привязываться к уже имеющемуся Presenter при смене конфигурации
- После привязывания View к уже имеющемуся Presenter, View должна отображать актуальное состояние Presenter
- Presenter должен уметь(при необходимости) жить независимо от того, кто на него подписан или от него отписался
На данный момент ни одно из существующих решений не умеет делать все эти пункты одновременно. Как нам сперва показалось, больше всего нам подходила библиотека Mosby. Но позже выяснилось, что при её использовании, нам пришлось бы писать слишком много кода, каждый раз. Особенно, для реализации первых двух пунктов из нашего списка требований. Поэтому было принято решение разработать собственное решение.
Moxy – теория
Наше решение сильно отличается от всех прочих(даже сама концепция MVP была модернизирована) тем, что между View и Presenter затесался ViewState. Причём он там абсолютно необходим. Он отвечает за то, чтобы каждая View всегда выглядела именно так, как того хочет Presenter. ViewState хранит в себе список команд, которые были переданы из Presenter во View. И когда „новая“ View присоединяется к Presenter, ViewState автоматически применяет к ней все команды, которые Presenter выдавал раньше. Таким образом получается, что не зависимо от того, что произойдёт со View по вине Android, View останется всё-равно в правильном состоянии. Для этого вам нужно будет только привыкнуть изменять View исключительно командами из Presenter. Заметим, что это одно из основных правил MVP и распространяется не только на Moxy.
Схематичная иллюстрация того, как это работает:
Что происходит на этой схеме:
- Во View происходит событие
, которое передаётся в Presenter
- Presenter передаёт команду
во ViewState
- Presenter стартует асинхронный запрос
в Model
- ViewState складывает команду
в очередь команд, после чего передаёт её во View
- View приводит себя в состояние, указанное в команде
- Presenter получает результат запроса
из Model
- Presenter передаёт во ViewState две команды
и
- ViewState сохраняет команды
и
в очередь команд и передаёт их во View
- View приводит себя в состояние, указанное в командах
и
- Новая/пересозданная View присоединяется к уже имеющемуся Presenter
- ViewState передаёт сохранённый список команд в новую/пересозданную View
- Новая/пересозданная View приводит себя в состояние, указанное в командах
,
и
Moxy – возможности
У Moxy есть несколько весомых преимущества перед другими решениями:
- Presenter не пересоздаётся при пересоздании Activity(это в разы упрощает работу с многопоточьностью)
- Автоматизация полного восстановления того, что видит пользователь при пересоздании Activity(в том числе при динамическом добавлении элементов Android View)
- Возможность из одного Presenter менять сразу несколько View(на практике оказалось чрезвычайно удобно)
Для этого в Moxy есть несколько механизмов, которые можно комбинировать между собой так, как вам будет угодно. Самыми весомыми механизмами являются аннотации, на основании которых генерируется код. А во время исполнения программы, инструмент под название MvpDelegate начинает полноценно использовать сгенерированный код.
Доступны следующие аннотации:
- @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 . В 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(все одного типа 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. И в первом, и во втором методе вы можете менять список команд как хотите.
Давайте рассмотрим стратегии, которые уже реализованы в Moxy:
- AddToEndStrategy – добавит пришедшую команду в конец очереди. Используется по умолчанию
- AddToEndSingleStrategy – добавит пришедшую команду в конец очереди команд. Причём, если команда такого типа уже есть в очереди, то уже существующая будет удалена
- SingleStateStrategy – очистит всю очередь команд, после чего добавит себя в неё
- SkipStrategy – команда не будет добавлена в очередь, и никак не изменит очередь
Если же у вас какая-то специфичная логика и вам не хватает этих стратегий, то вы можете сделать свою стратегию. В этом случае вам поможет механизм тегирования методов. В аннотацию @StateStrategyType можно передать параметр tag(по умолчанию является названием метода). Затем, по этому тегу, вы сможете в методах void beforeApply(List > currentState, ViewCommand incomingCommand) и void afterApply(List > currentState, ViewCommand incomingCommand) понять, что за ViewComand вам пришли(из метода ViewCommand String getTag() ).
Перед написанием своих стратегий, посмотрите на код уже реализованных – возможно он будет вам полезен.
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, этот механизм открывает очень много возможностей перед вами.
Второй вариант(для парметризированного тэга). По сути, он похож на первый вариант. Отличие лишь в том, что во втором случае вы не можете заранее знать, какой тэг будет у Presenter. Т.е. тэг должен генерироваться динамически. Тогда вам придётся немного постараться:
- Создайте свою реализацию PresenterFactory
- В аннотацию @InjectPresenter установите параметры:
- В factory установите класс вашей PresenterFactory
- В presenterId установите строковый идентификатор Presenter(это нужно для того, чтобы различать в одном классе Presenter с одинаковыми фабриками)
- Заведите свой интерфейс, содержащий один и только один метод, который будет возвращать параметр для factory нужного типа:
- Аннотируйте этот интерфейс как @ParamsProvider(PresenterFactoryClass), передав аннотации, в качестве параметра, класс вашей PresenterFactory
- Опишите метод, который будет возвращать параметр, должен на вход получать один параметр String (в этот параметр придёт тот самый параметр presenterId из аннотации @InjectPresenter)
- Объект, который содержит 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 . В таком случае будет легко сделать взаимодействие Model⇒Presenter, и в то же время Model⇔Model. Это даст возможность легко сделать параллельное исполнение запросов из 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-apt:
- Модифицируйте build.gradle вашего проекта:
Исходники библиотеки можно найти на Github: https://github.com/Arello-Mobile/Moxy
В момент, когда мы соберём репрезентативный список вопросов по нашей библиотеке, по тому, как её использовать, по MVP в целом, будет сделана отдельная статья, в которой будут освещены самые популярные/интересные вопросы. Вопросы можно задавать здесь в комментариях, писать мне(@senneco ) и ещё одному автору библиотеки – Xanderblinov. Или можете обращаться ко всему отделу Android-разработки Arello Mobile, написав на java-developers@arello-mobile.com.
От авторов библиотеки Moxy
senneco и Xanderblinov
Источник