Как стать автором
Обновить

MVP для Android — преимущества использования Moxy в качестве вспомогательной библиотеки

Время на прочтение8 мин
Количество просмотров12K

В данной статье описываются преимущества использования Moxy в качестве вспомогательной библиотеки при использовании MVP для Android-приложения.


Важно: здесь не сравнивается MVP с другими архитектурными подходами типа MVVM, MVI и т.п. В статье описывается почему, если в качестве архитектуры UI-ной части приложения выбран MVP, то лучше не использовать самописную реализацию MVP, а использовать Moxy.


Библиотека Moxy позволяет избежать boilerplate кода для обработки lifecycle фрагментов и активитей, и работать с View как будто оно всегда активно.
Далее под View понимается имплементация View в виде фрагмента или активити.
Под интерактором понимается сущность бизнес-логики, т.е. класс, который лежит на более низком уровне абстракции, чем Presenter.


Общие преимущества Moxy


  • Активная поддержка и разработка библиотеки.
  • Поддержка фичей Kotlin вроде val presenter by moxyPresenter { component.myPresenter } и presenterScope для корутин.
  • Автоматическое восстановление состояния View.
  • Автоматическая увязка с жизненным циклом (а отсюда отсутствие утечек Активити и прочей подобной прелести).
  • Обращения к View происходят через не-nullable viewState. Нет риска, что какая-то команда View потеряется.
  • Весь lifecycle экрана сводится к двум коллбэкам презентера — onFirstViewAttach() и onDestroy().
  • Время разработки экранов сокращается — не нужно писать лишний код для обработки lifecycle и сохранения состояний.

Типичные задачи и решения


Рассмотрим, как решаются типичные задачи при разработке UI с использованием Moxy и без.


При решениях без Moxy предполагается следующая типичная реализация MVP. В presenter хранится nullable-ссылка на view. Presenter аттачится (передаётся ссылка на View) при создании View (в onCreate()) и детачится (зануляется ссылка на View) при уничтожении View (в onDestroy()).


Задача: асинхронный запрос данных и отображение результата на UI


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


class MyPresenter
...
// Функция запрашивает список и отображает его на UI
override fun onDisplayListClicked() {
        myListInteractor.requestList()
            .subscribe { displayList(it) }

Решение с Moxy


private fun displayList(items: List<Item>) {
    viewState.setListItems(items)
}

Обращаемся к не-nullable viewState и передаём туда загруженные данные. Моху прикопает результат и отправит View, когда оно будет активно. При пересоздании View команда может быть повторена (зависит от стратегии) при этом заново данные не будут запрашиваться.


Решение без Moxy


private fun displayList(items: List<Item>) {
    view?.setListItems(items)
}

Обращаемся к View по nullable-ссылке. Эта ссылка зануляется при пересоздании View. Если к моменту завершения запроса view не приаттачена, то данные потеряются.
Возможное решение проблемы.


private fun displayList(items: List<Item>) {
    view?.let { it.setListItems(items)
         }?: let {
             cachedListInteractor.saveList(items) 
         }
}

Прикапывать данные в какой-то сущности, которая не связана с View (например, в интеракторе). При onResume() запрашивать прихранённые данные и отображать их.
Минусы решения.


  • Лишняя работа по сохранению результата из-за особенностей платформы (lifecycle Android активитей и фрагментов).
  • Прихранённые данные нужны только View, бизнес-логика будет перегружена проблемами сохранения результата.
  • Нужно следить за своевременной очисткой результата. Если объект, хранящий состояние, используется где-то ещё, то этот код также должен следить за его состоянием.
  • Presenter будет знать о View lifecycle, т.к. нужно уведомлять его об onResume(). Плохо, что Presenter знает об особенностях платформы.

Задача: сохранение состояния отображения


Часто возникает ситуация, когда нам нужно хранить какое-то состояние отображения.


Решение с Moxy


class MyPresenter
...
private var stateData: StateData = ...

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


Решения без Moxy


  • Можно хранить состояние в savedInstanceState или аргументах фрагмента.


    Минусы решения


    • View будет знать о state и, скорее всего, передавать его в presenter. Т.е. логика размазывается между View и presenter.
    • Возможно, понадобится явно из presenter обращаться к View с целью сохранить состояние, таким образом, в интерфейсе View будут «лишние» методы сохранения состояния (должны быть только методы для управления отображениям).
    • Presenter может не успеть обратиться к View для сохранения состояния, т.к. ссылка на View может занулиться и сам presenter может погибнуть.
    • Сохранить можно только примитивные и serializable/parcelable данные.
    • Boilerplate код для сохранения данных в Bundle и извлечения из Bundle.

  • Можно хранить данные на уровне бизнес-логики, в специальном классе (пусть этот класс будет отвечать только за state конкретной View и не будет использоваться где-то ещё).


    Минусы решения


    • Нужно следить за актуальностью данных.
    • Не просто разобраться, когда создавать и уничтожать класс, хранящий данные. Обычно его просто делают singleton'ом и он существует всегда, хотя нужен только одной View.


Задача: обмен данными между экранами


Часто бывает нужно результат действий на одном View отобразить на другом View.


Решение с Moxy


Обмен данными между экранами осуществляется так же как и асинхронный запрос. Разве что подписка на изменения идёт на subject или channel в интеракторе, в который presenter другой View кидает изменённые данные. Подписка в Presenter.onFirstViewAttach(), отписка — в Presenter.onDestroy().


Решения без Moxy


  • Как и выше, через интерактор с subject’ами или channel’ами. В этом случае подписываться/отписываться нужно в каждом onCreate()/onDestroy(). Так же есть риск потери данных, как в случае асинхронного запроса.
  • Через Broadcast или интент активити. Данные передаются через Bundle. Отсюда тот же Boilerplate с Bundle, как описано в разделе о сохранении состояния. Кроме того, в случае интента активити, логика по обмену данными ложится на View, хотя должна быть исключительно в presenter.

Задача: инициализация чего-либо, связанного с экраном


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


Решение с Moxy


Проинициализировать компонент можно в Presenter.onFirstViewAttach() и освободить в Presenter.onDestroy() — это единственные коллбэки, о которых нам нужно задумываться.
Presenter.onFirstViewAttach() — вызывается при самом первом создании View,
Presenter.onDestroy() — вызывается при окончательном уничтожении View.


Решение без Moxy


Можно проинициализировать в onCreate() и освободить в onDestroy() активити или фрагмента.
Минусы решения


  • Постоянная переинициализация компонента.
  • Если компонент содержит коллбеки, то возможна утечка памяти (объекта presenter или активити/фрагмента).

Задача: показ AlertDialog


Особенностью использования AlertDialog является то, что он пропадает при пересоздании View. Поэтому при пересоздании View нужно заново отображать AlertDialog.


class MyFragment : ... {

    private val myAlertDialog = AlertDialog.Builder(context)...

    override fun switchAlertDialog(show: Boolean) {
        if (show) myAlertDialog.show() else myAlertDialog.dismiss()
    }

Решение с Moxy


@StateStrategyType(AddToEndSingleStrategy::class)
fun switchAlertDialog(show: Boolean)

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


Решения без Moxy


  • Можно в View хранить состояние отображения AlertDialog и перепоказывать при пересоздании View. Получается лишний boilerplate просто чтобы восстановить диалог.
  • Можно использовать DialogFragment. Для простых диалогов — лишний overhead. И это добавляет проблемы с commit() фрагментов после onSaveInstanceState().

Особенности использования Moxy


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


  • Как команда View (т.е. вызов метода View) будет повторяться при пересоздании View – зависит от стратегии над данным методом интерфейса View. Важно понимать, что означают стратегии. Неправильный выбор стратегии может негативно сказаться на UX. К счастью, стандартных стратегий не много (всего 5) и они хорошо документированы, а создание кастомных стратегий требуется не часто.
  • Moxy обращается к View с того потока, с которого обратился к нему presenter. Поэтому нужно самостоятельно следить в presenter за тем, чтобы методы viewState вызвались из главного потока.
  • Moxy не решает проблему commit() фрагментов после выполнения onSaveInstanceState(). Разработчики Moxy рекомендуют использовать commitAllowingStateLoss(). Однако, это не должно вызывать беспокойство, т.к. за состояние View полностью отвечает Moxy. То, что где-то внутри android потеряется состояние View — нас не должно волновать.
  • Взаимно отменяющие команды view лучше объединять в один метод View. Например, скрытие и показ прогресса лучше сделать так:

@StateStrategyType(AddToEndSingleStrategy::class)
fun switchProgress(show: Boolean)

а не так:


@StateStrategyType(AddToEndSingleStrategy::class)
fun showProgress()

@StateStrategyType(AddToEndSingleStrategy::class)
fun hideProgress()

Либо можно использовать кастомную стратегию с тегами. Например, как описано тут: https://habr.com/ru/company/redmadrobot/blog/341108/
Это нужно чтобы команда показа прогресса не вызвалась больше после команды скрытия прогресса.


  • Presenter должен по-особому инжектится через dagger.
    Например, может возникнуть желание сделать так:

class MyFragment : ... {

    @Inject
    lateinit var presenter: MyPresenter

    @ProvidePresenter
    fun providePresenter(): MyPresenter {
        return presenter
    }

Так делать нельзя. Нужно чтобы функция @ProvidePresenter гарантированно создавала новый инстанс presenter. Здесь при пересоздании фрагмента появится новый инстанс presenter. А Moxy будет работать со старым, т.к. функция providePresenter() вызывается только один раз.
Как вариант, можно в providePresenter() просто создать новый инстанс presenter:


@ProvidePresenter
fun providePresenter(): MyPresenter {
    return MyPresenterImpl(myInteractor, schedulersProvider)
}

Это не очень удобно — ведь придётся инжектить в фрагмент все зависимости этого presenter.
Можно из компонента dagger сделать метод для получения presenter и позвать его в providePresenter():


@Component(modules = ...)
@Singleton
interface AppComponent {
...
    fun getMyPresenter(): MyPresenter

}

class MyFragment : ... {
...
@ProvidePresenter
fun providePresenter(): MyPresenter {
    return TheApplication.getAppComponent().getMyPresenter()
}

Важно, чтобы провайдер presenter’а и сам presenter не были помечены аннотацией Singleton.


В последних версиях Moxy можно использовать делегаты kotlin:


private val myPresenter: MyPresenter by moxyPresenter {    
    MyComponent.get().myPresenter 
}

Ещё можно заинжектить через Provider:


@Inject
lateinit var presenterProvider: Provider<MyPresenter>
private val presenter by moxyPresenter { presenterProvider.get() }

Итог


Moxy — замечательная библиотека, которая позволяет значительно упростить жизнь android-разработчика при использовании MVP.
Как и с любой новой технологией или библиотекой, в начале использования Moxy неизбежно возникают ошибки, например, не верный выбор стратегии или не правильный inject Presenter'а. Однако с опытом всё становится просто и понятно и уже сложно себе представить MVP без использования Moxy.
Выражаю благодарность сообществу Moxy за такой замечательный инструмент. А так же участникам telegram-чата Moxy за ревью и помощь в написании статьи.


Ссылки


Moxy — реализация MVP под Android с щепоткой магии – отличная статья от разработчиков Moxy с описанием того, для чего создавалась Moxy и как с ней работать.
Стратегии в Moxy (часть 1) — статья хорошо описывает стандартные стратегии Moxy.
Стратегии в Moxy (Часть 2) — руководство по созданию кастомных стратегий Moxy.
Об использовании популярных практик в разработке под Android – высокоуровнево описывается MVP и Moxy.
Moxy. Как правильно пользоваться? / Юрий Шмаков (Arello Mobile) – запись с конференции AppsConf, где разработчик Moxy рассказывает о том, как пользоваться библиотекой.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 7: ↑6 и ↓1+5
Комментарии8

Публикации