Android mvp moxy example

Android mvp moxy example

Буквально год назад коллеги по работе посоветовали мне 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 сформированы шесть базовых высокоуровневых команд:

  1. navigateTo(Screen)— переход на новый экран;
  2. newScreenChain(Screen) — сброс стека до корневого экрана и открытие одного нового;
  3. newRootScreen(Screen) — сброс стека и замена корневого экрана;
  4. replaceScreen(Screen) — замена активного экрана;
  5. backTo(Screen) — возврат на любой экран в стеке;
  6. exit()— выход с активного экрана.

Оба этих списка могут быть расширены под конкретные нужды разработчика. В первом случае нужно реализовать интерфейс Command, во втором — расширить класс Router.

Чтобы добавить анимацию переходов между экранами, нужно либо полностью реализовать интерфейс Navigator, либо переопределить у базового экземпляра SupportAppNavigator метод setupFragmentTransaction, в котором в качестве параметра имеется экземпляр класса FragmentTransaction. Именно ему нужно добавить либо Transition, либо Animation.

Предлагаю посмотреть небольшой пример использования Cicerone в связке с Moxy:

Источник

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

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

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

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

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

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

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

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

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

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

Решение с Moxy

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

Читайте также:  Android автоповорот экрана не работает

Решение без Moxy

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

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

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

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

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

Решение с Moxy

Храним состояние в 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

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

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

Решение с Moxy

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

Решение без Moxy

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

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

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

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

Читайте также:  Android shortcut to file

Решение с Moxy

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

Решения без Moxy

Особенности использования 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. Например, скрытие и показ прогресса лучше сделать так:

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

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

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

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

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

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

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

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 рассказывает о том, как пользоваться библиотекой.

Источник

Оцените статью