Через полгода после выхода прошлой статьи о сравнении RxPM c другими презентационными паттернами мы с Jeevuz, наконец, готовы представить библиотеку RxPM — реактивную реализацию паттерна Presentation Model. Давайте сделаем небольшой обзор основных компонентов библиотеки и покажем, как их использовать. Для начала посмотрим на общую схему:   Перейдем к рассмотрению основных компонентов библиотеки. Основная задача RxPM — описать все состояния в PresentationModel и предоставить возможность взаимодействовать с ними в реактивном стиле. Зачастую нам необходимо не только получать доступ к состоянию, но и реагировать на его изменения для синхронизации представления (View). Для этого в библиотеке есть класс State, который реализует реактивное свойство. Реактивное свойство — это разновидность свойства, которое уведомляет о своих изменениях и предоставляет реактивные интерфейсы для взаимодействия с ним. В статье про паттерн мы говорили что, нужно описать два свойства, чтобы скрыть от View доступ к изменению состояния: Это был один из раздражающих моментов в паттерне, поэтому мы решили обернуть BehaviorRelay в State и предоставить observable и consumer для взаимодействия с ним. Теперь можем писать в одну строчку: Во View подписываемся на изменения состояния: bindTo — расширение в библиотеке для привязки к реактивным свойствам Изменить состояние можно через consumer, который доступен только внутри PresentationModel: Так же как и у обычного свойства мы можем взять текущее значение состояния: Преимущество реактивного свойства не только в том, что можно наблюдать за его изменением, но также связывать и компоновать с другими реактивными свойствами. Так мы получаем новое состояние, которое будет зависеть и реагировать на изменения других. Например, можно блокировать кнопку на время выполнения запроса в сеть: untilDestroy — расширение в PresentationModel, которое добавляет Disposable в CompositeDisposable. Ещё один пример — включать и отключать кнопку в зависимости от заполненности полей в форме: Аналогично State этот класс инкапсулирует доступ к PublishRelay и предназначен для описания пользовательских действий, таких как нажатия на кнопки, переключения и т. п. Логичным вопросом будет, а не легче описать метод в PresentationModel, зачем объявлять свойство и подписываться на него? В некоторых случаях это справедливо. Например, если действие очень простое, такое как открытие следующего экрана или прямой вызов модели. Однако если нужно по клику сделать запрос в сеть, и при этом фильтровать нажатия во время прогресса, то в этом случае взаимодействие через Action предпочтительнее. Основное преимущество Action — это то, что он не разрывает Rx-цепочку. Объясню на примере. Вариант с методом: Как видно из примера выше, необходимо на каждый запрос объявлять переменную Disposable, чтобы при каждом новом клике завершать предыдущий запрос. А также не забыть отписаться в onDestroy. Это следствие того, что каждый раз когда вызывается метод sendRequest по клику на кнопку, создается новая Rx-цепочка. Вариант с Action: Используя Action, нужно только один раз инициализировать Rx-цепочку и подписаться на нее. Кроме того, мы можем использовать многочисленные полезные Rx-операторы, такие как debounce, filter, map и т. д.  Для примера рассмотрим задержку запроса при вводе строки для поиска: А в сочетании с RxBinding связывать View и PresentationModel ещё удобнее: Ещё одна важная проблема — это отображение ошибок и диалогов, либо других команд. Они не являются состоянием, так как должны выполниться один раз. Например, чтобы показать диалог, нам State не подойдет, так как при каждой подписке к State будет получено последнее значение, соответственно, каждый раз будет показываться новый диалог. Для решения этой проблемы был создан класс Command, который реализует желаемое поведение, инкапсулируя PublishRelay. Но что же произойдет, если послать команду в тот момент, когда View ещё не привязана к PresentationModel? Мы потеряем эту команду. Чтобы не допустить этого, мы предусмотрели буфер, который накапливает команды, пока View отсутствует, и посылает их когда View привязывается. Когда View привязана к PresentationModel, то Command работает так же, как PublishRelay. По умолчанию буфер накапливает неограниченное количество команд, но можно задать конкретный размер буфера: Если нужно сохранять только последнюю команду: Если указать 0, то Command будет работать как PublishRelay: Привязываемся во View: Наиболее наглядно работу Command демонстрирует marble-диаграмма: По умолчанию буфер включается при привязке View к PresentationModel. Но можно реализовать свой механизм, задав открывающий/закрывающий observable.  Так, например, при работе с Google Maps, признаком готовности View является не только привязка к PresentationModel, но и готовность карты. В библиотеке уже есть готовая команда для работы с картой: Мы описали основные примитивы RxPM: State, Action и Command, из которых строится PresentationModel. Теперь разберём базовый класс PresentationModel. В нём ведётся вся основная работа с жизненным циклом. Всего у нас имеется 4 callback-а: Также можно отслеживать жизненный цикл через lifecycleObservable. Для удобной отписки есть расширения Disposable, доступные в PresentationModel: На onBind и onDestroy очищаются compositeUnbind и compositeDestroy соответственно. Давайте рассмотрим пример работы с PresentationModel:
Необходимо по Pull To Refresh посылать запрос в сеть и обновлять данные на экране, во время запроса отображать прогресс, а в случае ошибки показывать пользователю диалог с сообщением. Для начала нужно определить, какие состояния и команды нужны для View и какие пользовательские события мы можем получать от View: Теперь нужно связать свойства и модель в методе onCreate: Когда PresentationModel спроектирована, то остается только привязать её ко View.
В библиотеке уже реализованы базовые классы для реализации PmView: PmSupportActivity, PmSupportFragment и PmController (для пользователей фреймворка Conductor). Каждый из них реализует интерфейс AndroidPmView и прокидывает нужные callback-и в соответствующий делегат, который управляет жизненным циклом PresentationModel и обеспечивает корректное сохранение её во время поворота экрана. Наследуемся от PmSupportFragment и реализуем всего два обязательных метода: bindTo — это удобное расширение в AndroidPmView. Используя его, вам не нужно беспокоится об отписке от свойств из PresentationModel и переключении на главный поток. Для работы с Google Maps в библиотеке есть дополнительные базовые классы: MapPmSupportActivity, MapPmSupportFragment и MapPmController. В них добавляется отдельный метод для привязки GoogleMap: В этом методе мы можем отображать пины на карте, передвигать и анимировать местоположение и т. п. Пока мы рассматривали только одностороннее изменение State, когда PresentationModel изменяет состояние, а View подписывается на него. Но достаточно часто возникает потребность изменять состояние с двух сторон. Классический пример — это поле ввода: его значение может изменить как пользователь так и PresentationModel, инициализируя начальным значением или форматируя ввод. Такая связка носит название двустороннего датабиндинга. Покажем на схеме, как она реализуется в RxPM: Пользователь вводит текст ➔ срабатывает слушатель ➔ изменение передается в Action ➔ PresentationModel фильтрует и форматирует текст и подставляет его в State ➔ измененное состояние получает View ➔ текст подставляется в поле ввода ➔ срабатывает слушатель ➔ круг замыкается и система впадает в бесконечный цикл. Мы написали класс InputControl, который реализует эту двустороннюю связку для полей ввода и решает проблему зацикливания. Объявляем в PresentationModel: Привязываемся во View через привычный bindTo Также можно задать форматтер:  Аналогичную проблему зацикливания и двустороннего связывания для CheckBox решает CheckControl.  Мы рассмотрели основные классы и особенности библиотеки. Вот далеко не полный список фич, которые есть в RxPM:  Библиотека написана на Kotlin и использует RxJava2.
RxPM используется уже в нескольких приложениях в продакшне и показала стабильность в своей работе. Но мы продолжаем над ней работать, есть много идей для дальнейшего развития и улучшения. Недавно вышла версия 1.1 с очень полезной фичей для навигации, но об этом мы поговорим в следующей статье.  Только одной статьи будет недостаточно, чтобы понять возможности RxPM. Поэтому пробуйте, смотрите исходники и примеры, задавайте свои вопросы. Мы будем рады обратной связи. RxPM: https://github.com/dmdevgo/RxPM
Sample: https://github.com/dmdevgo/RxPM/tree/develop/sample
Чат в телеграм: https://t.me/Rx_PM 24 ноября (в эту пятницу) я выступлю с мини-докладом про RxPM на Droidcon Moscow 2017. Приходите — пообщаемся. Developer