Что такое 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
- @GenerateViewState – аннотация для генерации кода ViewState для определенного интерфейса View
- @InjectViewState – аннотация для привязывания ViewState к Presenter
- @StateStrategyType – аннотация для управления стратегией добавления и удаления команды из очереди команд во ViewState
Обо всём этом далее.
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.
@InjectViewState
public class MyPresenter<T extends MvpView> extends MvpPresenter<T>
{
// pass
}
Annotation processor не правильно поймёт класс View, ViewState которого нужно использовать. В таком случае вы можете явно передать класс View в параметр view аннотации @InjectViewState.Поэтому такой код писать можно:
@GenerateViewState
public interface ConcreteInterface extends AbstractInterface<String>
{
// pass
}
А такой код писать нельзя:
@GenerateViewState
public interface ConcreteInterface<Type> extends AbstractInterface<Type>
{
// pass
}
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<Pair<ViewCommand<View>, Object>> currentState, Pair<ViewCommand<View>, Object> incomingState)
и void afterApply(List<Pair<ViewCommand<View>, Object>> currentState, Pair<ViewCommand<View>, Object> incomingState)
понять, что за 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 установите класс вашей
- Заведите свой интерфейс, содержащий один и только один метод, который будет возвращать параметр для factory нужного типа:
- Аннотируйте этот интерфейс как @ParamsProvider(PresenterFactoryClass), передав аннотации, в качестве параметра, класс вашей
PresenterFactory
- Опишите метод, который будет возвращать параметр, должен на вход получать один параметр
String
(в этот параметр придёт тот самый параметр presenterId из аннотации @InjectPresenter)
- Аннотируйте этот интерфейс как @ParamsProvider(PresenterFactoryClass), передав аннотации, в качестве параметра, класс вашей
- Объект, который содержит 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
.
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 не получилась бы такой, какой она получилась, если бы не многочисленные труды других людей. Вот некоторые из них:
- Android Application Architecture (Android Dev Summit 2015)
- Android Testing Codelab – тема MVP затронута не сильно, но можно что-то для себя почерпнуть. Так же можно посмотреть, как тестировать MVP.
- Nucleus – пример реализации MVP, с замашкой на обработку жизненного цикла.
- Mosby – лучшая реализация MVP до релиза Moxy. Отлично расписаны сами принципы MVP, которые критично понять.
- Old Mosby – руководство к первой версии Mosby. Крайне полезное для понимания того, что такое MVP.
- STINSON'S PLAYBOOK FOR MOSBY – набор советов, который очень поможет вам определиться с некоторыми понятиями MVP. Так же дополнительно объяснит, какая часть программы, каким компонентом должна стать.
- Android Reactive MVP: практика – ещё одно видение, как должна выглядит структура Android-приложения, построенного на MVP
- Andrtoid Clean Architecture – поможет понять, что такое модель, и на какие компоненты её можно разложить.
- Алексей Макаров. Speaker Clean Architecture и MVP – хороший доклад про Clean Architecture, и хорошие вопросы в конце.
- Mosby issues 85 – помогает понять, что из себя должен представлять Repository.
Moxy – где брать
Чтобы подключить Moxy в свой проект, просто добавьте её в зависимостях. Moxy состоит из трёх частей. Одна из них отвечает за предоставление вам Moxy SDK. Её довольно просто подключить:
dependencies{
...
compile 'com.arello-mobile:moxy:0.3.3'
}
Если вы хотите иметь доступ к таким вспомогательным классам, как
MvpApplication
, MvpActivity
и MvpFragment
, так же подключите moxy-android
:
dependencies{
...
compile 'com.arello-mobile:moxy-android:0.3.2'
}
Другая часть отвечает за обработку аннотаций и занимается генерацией кода. И здесь вам нужно определиться.
Если у вас нет никаких особых требований, ваш проект – обычный Android-проект, и вы не хотите, чтобы сгенерированный код был доступен из вашего когда, то подключите зависимость так:
dependencies{
...
provided 'com.arello-mobile:moxy-compiler:0.3.3'
}
Если же вы хотите иметь прямой доступ к сгенерированному коду, то стоит использовать android-apt:
- Модифицируйте build.gradle вашего проекта:
buildscript { dependencies { classpath 'com.neenbedankt.gradle.plugins:android-apt:1.4' } }
- Модифицируйте build.gradle вашего приложения:
apply plugin: 'com.neenbedankt.android-apt' dependencies { ... apt 'com.arello-mobile:moxy-compiler:0.3.3' }
Исходники библиотеки можно найти на Github: http://ift.tt/1LkYH4h
Полноценный пример приложения, использующего Moxy: http://ift.tt/1QbZXOz
В момент, когда мы соберём репрезентативный список вопросов по нашей библиотеке, по тому, как её использовать, по MVP в целом, будет сделана отдельная статья, в которой будут освещены самые популярные/интересные вопросы. Вопросы можно задавать здесь в комментариях, писать мне( senneco ) и ещё одному автору библиотеки – Xanderblinov. Или можете обращаться ко всему отделу Android-разработки Arello Mobile, написав на java-developers@arello-mobile.com.
От авторов библиотеки Moxy
senneco и Xanderblinov
This entry passed through the Full-Text RSS service - if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.
Комментариев нет:
Отправить комментарий