Уже продолжительное время я размышляю над паттерном RxPM и даже успешно применяю его в «продакшне». Я планировал сначала выступить с этой темой на Mobius, но программный комитет отказал, поэтому публикую статью сейчас, чтобы поделиться с Android-сообществом своим видением нового паттерна.


Все знакомы с MVP и MVVM, но мало кто знает, что MVVM является логическим развитием паттерна Presentation Model. Ведь единственное отличие MVVM от PM – это автоматическое связывание данных (databinding).


В этой статье речь пойдет о паттерне Presentation Model с реактивной реализацией биндинга. Некоторые ошибочно называют его RxMVVM, но корректно будет называть его RxPM, потому что это модификация шаблона Presentation Model.


Этот паттерн удобно использовать в проектах с Rx, так как он позволяет сделать приложение по-настоящему реактивным. Кроме того, он не имеет многих проблем других паттернов. На диаграмме ниже представлены различные варианты и классификации шаблонов представления:




Прежде, чем перейти к описанию паттерна RxPM, давайте рассмотрим самые популярные из них — MVP (Passive View) и MVVM. Подробное описание всех паттернов и их различий вы можете прочитать в предыдущей статье.


MVP vs PM vs MVVM


Общую схему паттернов можно представить в виде диаграммы:



С первого взгляда может показаться, что принципиальной разницы между ними нет. Но это только на первый взгляд. Различия заключаются в обязанностях посредника и его способе связи со View. Модель же выглядит во всех паттернах одинаково. Ее проектирование – это сложная и обширная тема, не будем сейчас останавливаться на ней. Начнем с самого популярного паттерна – MVP в варианте Passive View. Рассмотрим его основные проблемы.


MVP


В классическом MVP ответственность за сохранение и восстановление состояния UI лежит на View. Presenter только отслеживает изменения в модели, обновляет View через интерфейс и, наоборот, принимает команды от View и изменяет Model.


Однако при реализации сложных интерфейсов, помимо состояния данных в модели, есть дополнительные состояния UI, которые никак не связаны с данными. Например, какой элемент списка выделен на экране или какими данными заполнены форма ввода, информация о ходе процесса загрузки или запросов в сеть. Восстановление и сохранение UI-состояния во View доставляет большие проблемы, так как View имеет обыкновение «умирать». А информацию о сетевых запросах View в принципе не способна сохранить. Пока View отсоединена от презентера, запрос, скорее всего, завершится с каким-нибудь результатом.


Поэтому работу восстановления состояния UI выносят в презентер. Для этого требуется хранить в презентере дополнительные данные и флаги о текущем состоянии и воспроизводить его при каждом присоединении View.


Вторая проблема вытекает из того же условия, что View может быть в любой момент отсоединена от презентера, например, при повороте экрана. Соответственно, ссылка на интерфейс View в презентере будет обнулена. Поэтому нужно всегда делать проверку на null, когда требуется обновить View. Это довольно утомительно и захламляет код.


Третья проблема: необходимо довольно детально описывать интерфейс View, так как она должна быть как можно более «тупой». А презентеру приходится вызывать множество методов, чтобы привести View в нужное состояние. Это увеличивает количество кода.


PM


Существует другой паттерн под названием Presentation Model, который описал Martin Fowler. Суть этого паттерна заключается в том, что вводится специальная модель, называемая «моделью представления», которая хранит состояние UI и содержит UI-логику. PresentationModel следует рассматривать как абстрактное представление, которое не зависит от какого-либо GUI-фреймворка. PresentationModel хранит состояние в виде свойств (property), которые затем считывает View и отображает на экране. Основная проблема паттерна – это синхронизация состояния PresentationModel и View. Вам придется об этом позаботиться самостоятельно, применив паттерн «Наблюдатель». Скорее всего, потребуется отслеживать изменения каждого свойства, чтобы не обновлять UI целиком. Получится довольно много скучного и повторяющегося кода.


MVVM


Как вы могли заметить, MVVM очень похож на Presentation Model. Не удивительно, ведь он является его развитием. Только PresentationModel называется ViewModel, а синхронизация состояния ViewModel и View осуществляется с помощью автоматического связывания данных, т. е. датабиндинга. Но и этот паттерн не лишен недостатков. Например, в нем проблематично «чисто» реализовать какие-нибудь анимации или что-либо сделать со View из кода. Об этом подробнее можно почитать в статье моего коллеги Jeevuz.


Немного забегая вперед, вот его комментарий из наших обсуждений MVVM и RxPM
Начав обсуждать и обдумывать RxPM я понял, что этот паттерн объединяет в себе то, что мне нравилось в MVVM — понятие ViewModel'и как интерфейса над View, но в то же время не содержит в себе основного недостатка — двойственности. Что логично, ведь нет databinding'a. Но при этом биндинг при помощи Rx не намного сложнее автоматического биндинга c Databinding Library, и при этом очень хорошо подходит для применения в реактивных приложениях.
Как следствие, RxPM решает и проблему состояний. Помните про кубик рубик из моей статьи? Я описывал, что состояние можно описать либо набором полей, либо набором действий… Так вот, RxPM интересным способом объединяет в себе эти два способа: PresentationModel хранит состояния View как набор полей, но так как эти поля представлены BehaviorSubject'ами (которые испускают последнее событие при подписке), то они одновременно являются и «действиями». И получается, что любое событие произошедшее в фоне (пока не было View) прилетит во время подписки. Отлично!

Но самым главным и решающим недостатком всех вышеперечисленных паттернов является то, что взаимодействие View и посредника осуществляется в императивном стиле. Тогда как наша цель – это написание реактивных приложений. UI-слой – это довольной большой источник потока данных, особенно в динамичных интерфейсах, и было бы опрометчиво использовать Rx только для асинхронной работы с моделью.


Реактивный Presentation Model


Мы уже выяснили, что основная проблема паттерна Presentation Model – это синхронизация состояния между PresentationModel и View. Очевидно, что необходимо использовать observable property – свойство, которое умеет уведомлять о своих изменениях. В решении этой задачи нам как раз и поможет RxJava, а заодно мы получим все плюсы реактивного подхода.


Для начала посмотрим на схему паттерна и далее будем разбираться в деталях реализации:


Итак, ключевым элементом RxPM является реактивное property. Первым кандидатом на роль Rx-property напрашивается BehaviorSubject. Он хранит последнее значение и отдает его каждый раз при подписке.


Вообще Subject’ы уникальны по своей природе: с одной стороны, они являются расширением Observable, а с другой, реализуют интерфейс Observer. То есть мы можем использовать Subject как исходящий поток данных для View, а в PresentationModel он будет потребителем входящего потока данных.


Однако у Subject’ов есть недостатки, которые для нас неприемлемы. По контракту Observable они могут завершаться с событиями onComplete и onError. Соответственно, если Subject будет подписан на что-то, что завершится с ошибкой, то вся цепочка будет остановлена. View перестанет получать события и придется подписываться заново. Кроме того, Rx-property по определению не может посылать события onComplete и onError, так как является всего лишь источником данных (состояния) для View. Тут нам на помощь приходит Jake Wharton со своей библиотекой RxRelay. Что бы мы без него делали? Relay’и лишены описанных недостатков.


В арсенале у нас несколько подклассов:


  • BehaviorRelay – хранит последнее полученное значение и рассылает его каждый раз при подписке. Лучше всего подходит для хранения и изменения состояний.


  • PublishRelay – просто горячий Observable. Подойдет для каких-нибудь команд или событий для View. Например, чтобы показать диалог или запустить анимацию. Также используется для получения команд (событий) от View.


  • ReplayRelay – сохраняет все полученные элементы в буфер и воспроизводит их все при подписке. Крайне редко используется, но может помочь для составных состояний. На ум приходит пример с рисованием: нарисовать линию, потом круг и т. д.

Но мы не можем предоставить доступ View к Relay’ям напрямую. Так как она может случайно положить значение в property или подписаться на Relay, который предназначен для получения команд от View. Поэтому требуется представить свойства в виде Observable, а слушатели событий от View как Consumer. Да, инкапсуляция потребует больше кода, но с другой стороны будет сразу понятно, где свойства, а где команды. Пример с прогрессом загрузки в PresentationModel (pm):


//State
private val progress = BehaviorRelay.create<Int>()
// можно в виде property
val progressState: Observable<Int> = progress.hide()
// или в виде функции, если хочется такое же название
fun progress(): Observable<Int> = progress.hide()

//Action
private val downloadClicks = PublishRelay.create<Unit>()
// можно в виде property
val downloadClicksConsumer: Consumer<Unit> = downloadClicks
// или в виде функции, если хочется такое же название
fun downloadClicks(): Consumer<Unit> = downloadClicks

Теперь, когда мы определили стейты и экшены, нам остается только привязаться к ним во View. Для этого нам нужна еще одна библиотека Джейка Вортона — RxBinding. Когда он спит вообще?


pm.progressState.subscribe { progressBar.progress() } // привязываем состояние прогресса
downloadButton.clicks().subscribe { pm.downloadClicksConsumer } // прокидываем клики в PM

Если нет подходящего Observable, то можно вызывать consumer.accept() – напрямую из слушателя виджета.


pm.downloadClicksConsumer.accept(Unit)

А теперь на практике


Теперь соберем все вышесказанное в кучу и разберем на примере. Проектирование PresentationModel можно разбить на следующие шаги:


  1. Определить, какие состояния будет хранить PresentationModel, которые потребуются для View: данные, состояние загрузки, ошибки, которые нужно отобразить и т. п.
  2. Определить, какие события могут происходить во View: клики на кнопки, заполнение полей ввода и т. д.
  3. При создании PresentationModel связать состояния, команды и модель в декларативном стиле, как это позволяет нам Rx.
  4. Привязать View к PresentationModel.

Возьмем для примера задачу поиска слов в тексте:


  • Есть поле ввода для текста, в котором будем искать.
  • Есть поле ввода для слова/части, которое будем искать.
  • По клику на кнопку мы запускаем поиск.
  • Отображаем прогресс во время поиска, на это время блокируем кнопку.
  • После получения ответа отображаем список найденных слов.

Алгоритм поиска скроем за фасадом интерактора:


data class SearchParams(val text: String, val query: String)

interface Interactor {
    fun findWords(params: SearchParams): Single<List<String>>
}

class InteractorImpl : Interactor {
    override fun findWords(params: SearchParams): Single<List<String>> {
        return Single
                .just(params)
                .map { (text, query) ->
                    text
                            .split(" ", ",", ".", "?", "!", ignoreCase = true)
                            .filter { it.contains(query, ignoreCase = true) }
                }
                .subscribeOn(Schedulers.computation())
    }
}

В конкретном примере можно было бы обойтись вообще без Single и Rx, но мы сохраним однообразность интерфейсов. Тем более в реальных приложениях мог быть запрос в сеть через Retrofit.


Далее спроектируем PresentationModel.


Состояния для View: список найденых слов, состояние загрузки, флаг активности кнопки поиска. Состояние enabled для кнопки мы можем привязать к флагу загрузки в PresentationModel, но для View мы должны предоставить отдельное свойство. Почему бы просто не привязаться к флагу загрузки во View? Тут мы должны определить, что состояния у нас два: loading и enabled, но в данном случае так совпало, что PresentationModel их связывает. Хотя в общем случае они могут быть независимыми. Например, если бы понадобилось блокировать кнопку до тех пор, пока пользователь не введет минимальное количество символов.


События от View: ввод текста, ввод поискового запроса и клик по кнопке. Тут все просто: фильтруем тексты, объединяем текст и строку поиска в один объект — SearchParams. По клику на кнопку делаем поисковый запрос.


Вот как это выглядит в коде:


class TextSearchPresentationModel {

    private val interactor: Interactor = InteractorImpl()

    // --- States ---
    private val foundWords = BehaviorRelay.create<List<String>>()
    val foundWordState: Observable<List<String>> = foundWords.hide()

    private val loading = BehaviorRelay.createDefault<Boolean>(false)
    val loadingState: Observable<Boolean> = loading.hide()

    val searchButtonEnabledState: Observable<Boolean> = loading.map { !it }.hide()
    // --------------

    // --- UI-events ---
    private val searchQuery = PublishRelay.create<String>()
    val searchQueryConsumer: Consumer<String> = searchQuery

    private val inputTextChanges = PublishRelay.create<String>()
    val inputTextChangesConsumer: Consumer<String> = inputTextChanges

    private val searchButtonClicks = PublishRelay.create<Unit>()
    val searchButtonClicksConsumer: Consumer<Unit> = searchButtonClicks

    // ---------------

    private var disposable: Disposable? = null

    fun onCreate() {

        val filteredText = inputTextChanges.filter(String::isNotEmpty)
        val filteredQuery = searchQuery.filter(String::isNotEmpty)

        val combine = Observable.combineLatest(filteredText, filteredQuery, BiFunction(::SearchParams))

        val requestByClick = searchButtonClicks.withLatestFrom(combine,
                BiFunction<Unit, SearchParams, SearchParams> { _, params: SearchParams -> params })

        disposable = requestByClick
                .filter { !isLoading() }
                .doOnNext { showProgress() }
                .delay(3, TimeUnit.SECONDS) // делаем задержку чтобу увидеть прогресс
                .flatMap { interactor.findWords(it).toObservable() }
                .observeOn(AndroidSchedulers.mainThread())
                .doOnEach { hideProgress() }
                .subscribe(foundWords)
    }

    fun onDestroy() {
        disposable?.dispose()
    }

    private fun isLoading() = loading.value
    private fun showProgress() = loading.accept(true)
    private fun hideProgress() = loading.accept(false)
}

В роли View у нас будет выступать фрагмент:


class TextSearchFragment : Fragment() {

    private val pm = TextSearchPresentationModel()
    private var composite = CompositeDisposable()

    private lateinit var inputText: EditText
    private lateinit var queryEditText: EditText
    private lateinit var searchButton: Button
    private lateinit var progressBar: ProgressBar
    private lateinit var resultText: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        retainInstance = true //не умираем при поворотах экрана
        pm.onCreate()
    }

    // ... onCreateView

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        // ... init widgets

        onBindPresentationModel()
    }

    fun onBindPresentationModel() {

        // --- States ---
        pm.foundWordState
                .subscribe {
                    if (it.isNotEmpty()) {
                        resultText.text = it.joinToString(separator = "\n")
                    } else {
                        resultText.text = "Nothing found"
                    }
                }
                .addTo(composite)

        pm.searchButtonEnabledState
                .subscribe(searchButton.enabled())
                .addTo(composite)

        pm.loadingState
                .subscribe(progressBar.visibility())
                .addTo(composite)
        // ---------------

        // --- Ui-events ---
        queryEditText
                .textChanges()
                .map { it.toString() }
                .subscribe(pm.searchQueryConsumer)
                .addTo(composite)

        inputText
                .textChanges()
                .map { it.toString() }
                .subscribe(pm.inputTextChangesConsumer)
                .addTo(composite)

        searchButton.clicks()
                .subscribe(pm.searchButtonClicksConsumer)
                .addTo(composite)
        //------------------
    }

    fun onUnbindPresentationModel() {
        composite.clear()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        onUnbindPresentationModel()
    }

    override fun onDestroy() {
        super.onDestroy()
        pm.onDestroy()
    }
}

// Расширение из RxKotlin
/**
 * Add the disposable to a CompositeDisposable.
 * @param compositeDisposable CompositeDisposable to add this disposable to
 * @return this instance
 */
fun Disposable.addTo(compositeDisposable: CompositeDisposable): Disposable
        = apply { compositeDisposable.add(this) }

Полный пример вы можете посмотреть на GitHub.


Подведем итоги


Мы познакомились c новым паттерном RxPM и разобрали минусы других шаблонов представления. Но я не хочу однозначно сказать, что MVP и MVVM хуже или лучше RxPM. Я также, как и многие люблю MVP за его простоту и прямолинейность. А MVVM хорош наличием автоматического датабиндинга, хотя код в верстке – это на любителя.


Но в современных приложениях с динамичным UI очень много событийного и асинхронного кода. Поэтому мой выбор склоняется в сторону реактивного подхода и RxPM. Приведу слова из презентации Джейка Вортона, почему наши приложения должны быть реактивными:


Unless you can model your entire system synchronously, a single asynchronously source breaks imperative programming.
Если вы не можете смоделировать всю систему синхронно, то даже один асинхронный источник ломает императивное программирование.

Разумеется, у RxPM есть как плюсы, так и минусы.

Плюсы:


  • Позволяет не разрывать реактивные цепочки Observable и протягивать их от модели до View и наоборот. Это избавляет от императивного взаимодействия со View.
  • Декларативное описание логики в PresentationModel.
  • PresentationModel представляет собой абстракцию View, не завязана на конкретные виджеты.
  • Не нужно беспокоиться о том, присоединена View или нет. Просто меняем значение Rx-property. View автоматически получит стейт, когда подпишется.
  • События от View получаем в реактивном стиле, удобно применять операторы Rx, чтобы фильтровать, объединять и т. д.

Минусы:


  • Необходимо писать код связывания, но c Rx это легко.
  • Требуется инкапсулировать Relay’и, представлять их для View в виде Observable и Consumer. Это пока единственное, что напрягает меня.
  • Обилие Rx. Можно рассматривать и как минус, и как плюс. Реактивность – это другая парадигма, поэтому не удивительно, что начав использовать Rx, приходится использовать его повсюду. Никто же не трубит по поводу множества объектов при программировании на Java – это парадигма ООП.

Это, наверное, не полный список. Напишите в комментариях, какие вы видите плюсы и минусы, будет интересно узнать ваше мнение.


Итак, если вы чувствуете себя уверенно c Rx и хотите писать реактивные приложения, если вы устали от MVP и MVVM c databinding, то вам стоит попробовать RxPM. Ну а если вам и так комфортно, то не буду вас уговаривать ;)


P. S.


Искушенный Android-разработчик, скорее всего, заметил, что я ничего не говорил о жизненном цикле и о сохранении PresentationModel во время поворота. Эта проблема не специфична для данного паттерна и заслуживает отдельного рассмотрения. В своей статье я хотел сосредоточиться на самой сути паттерна: его плюсах и минусах в сравнении с MVP и MVVM. Также не были затронуты такие важные темы, как двусторонний databinding, навигация между экранами в контексте RxPM и некоторые другие. В следующей статье мы c Jeevuz постараемся рассказать о том, как начать использовать RxPM в реальном проекте и представим некоторое библиотечное решение, упрощающее его применение.

Поделиться с друзьями
-->

Комментарии (34)


  1. snuk182
    20.04.2017 19:02
    -1

    Поставьте пожалуйста тег Kotlin. Я уж чуть было не подумал, что промахнулся статьей, глядя на листинги с незнакомым синтаксисом.


  1. deej
    20.04.2017 22:56

    Насколько я понял, это все-таки просто MVVM, в котором ViewModel называется PresentationModel, а в качестве механизма связывания данных используется Rx.


    1. dmdev
      20.04.2017 23:17

      MVVM — это модификация более общего паттерна Presentation Model, со специфической реализацией автоматического связывания данных. Причем датабиндинг зависит от конкретной UI-платформы. В случае с RxPM мы имеем полуавтоматический биндинг: приходится вручную подписываться на Observable и отписываться от него. Стоит различать форму связывания, поэтому не совсем корректно называть представленный паттерн как RxMVVM.


      1. deej
        21.04.2017 13:55
        -2

        Вручную или автоматически, это субъективно. В том же WPF и прочих XAML само ничего не происходит, во View обязательно должно быть указано, к каким частям ViewModel она привязана, иначе это уже какая-то магия. В вашей реализации связывание происходит в onBindPresentationModel. Возможно, оно выглядит непривычно, т.к. описано не внутри разметки. Но где ему быть, это детали конкретной реализации, а суть в точности та же.


        Если совсем абстрагироваться от деталей и посмотреть еще раз, то в понимании MVVM Fragment — это слой View, и в нем таким же образом, как в XAML или XML (на примере Android Data Binding Library) описаны привязки. Я вижу MVVM.


        1. AlexeyKorshun
          21.04.2017 17:15
          -1

          Полностью согласен, ведь в данном случае binding это и есть Rx, мнение о том что MVVM в андроиде без data binding library не может существовать ошибочно.


    1. Mujahit
      21.04.2017 00:49
      +1

      Не совсем. Если почитать о MVVM, то становится ясно, что data binding — это один из столпов MVVM. А согласно описанию data binding, он должен сам связывать вьюшку с вьюмоделью, в то время как с Rx это приходится делать самому.
      Более того, давайте обратимся к истокам. Однажды Джон Госсман (человек, который и придумал паттерн MVVM) написал статью в которой рассказывал и о PresentationModel. По его же словам В PresentationModel раздражала ручная синхронизация вьюшки и модели. Его хотелось минимизировать. В результате чего в WPF и придумали data binding.
      Так что автор все правильно написал. Ведь, грубо говоря, если из MVVM убрать data binding, то получим PresentationModel.


      1. deej
        21.04.2017 14:00

        Не совсем. Если почитать о MVVM, то становится ясно, что data binding — это один из столпов MVVM. А согласно описанию data binding, он должен сам связывать вьюшку с вьюмоделью, в то время как с Rx это приходится делать самому.

        Может, data binding сам должен обновлять View? Связывание само не происходит, в том или ином виде оно всегда описано. Содержание остается тем же, а несущественная разница в форме не делает его "ручным" или "автоматическим".


        Так что автор все правильно написал. Ведь, грубо говоря, если из MVVM убрать data binding, то получим PresentationModel.

        Верно, и автор как раз добавил data binding в форме Rx.


        1. Jeevuz
          24.04.2017 08:57
          +1

          Не путайте людей, пожалуйста.
          Прочтите статьи по ссылкам. В описании паттерна Presentation Model все очень доступно описано.


          Суть отличия не просто в наличии databinding'а. Он есть и там и там. Data binding в прямую переводится как связывание данных. Без этого все паттерны были бы бесполезны.
          Отличие — в наличии автоматического датабиндинга.
          В случае с PM его не было. И позже, в WPF его добавили (позже). То есть MVVM развился из PM
          Поэтому называть PM как MVVM это как называть мотоцикл автомобилем.


          1. deej
            24.04.2017 11:52

            Я как раз никого не путаю, а напротив, пытаюсь развеять заблуждения.
            Статьи я прочел (почему вы решили иначе?).


            И позже, в WPF его добавили (позже).

            Неверно, data binding в WPF присутствовал с момента первого релиза (WPF 3.0).


            Поэтому называть PM как MVVM это как называть мотоцикл автомобилем.

            Согласен. Но называть автомобиль мотоциклом я также не буду.


            Суть отличия не просто в наличии databinding'а. Он есть и там и там. Data binding в прямую переводится как связывание данных. Без этого все паттерны были бы бесполезны.
            Отличие — в наличии автоматического датабиндинга.

            Того, на чем вы так сильно ставите акцент, нет ни по одной ссылке. Что для вас автоматическое, а что нет?


            Думаю, вы не там ищете разницу.


            Было бы очевидно, что все делается вручную, если бы все ивенты по старинке обрабатывались вручную, и в каждом обработчике в императивном стиле изменялись бы зависимые свойства View или PM. Такое никак нельзя было бы назвать MVVM.
            Здесь же, благодаря Rx, присутствует "автоматическое" оповещение об изменениях свойств, и все связи описаны декларативно в том самом onBindPresentationModel подобно тому, как это делается в разметке в других реализациях.


            Приведу цитату из статьи по второй ссылке. Совсем короткая, но в ней собрано всё, чтобы понять суть:


            If the binding has the correct settings and the data provides the proper notifications, then, when the data changes its value, the elements that are bound to the data reflect changes automatically.

            Все это присутствует у автора:


            • data provides the proper notifications = Observable типы в PM
            • if the binding has the correct settings = onBindPresentationModel написан без ошибок
            • when the data changes its value, the elements that are bound to the data reflect changes automatically = после "активации" связей путем однократного выполнения onBindPresentationModel элементы View автоматически меняются вслед за изменением VMPM, и наоборот (например, для полей ввода)


            1. Jeevuz
              24.04.2017 16:55
              +1

              А обычный listener оповещает не автоматически?
              Тогда PM вообще не существует как паттерн и есть только MVVM.
              Ну что ж, ок, только не говорите об этом Фаулеру, не расстраивайте человека ;)


              1. deej
                24.04.2017 21:42
                -1

                Попытка съязвить не усилит вашу аргументацию.


                А обычный listener оповещает не автоматически?

                Оповещает, конечно. Просто он некрасив.
                Обратимся к Фаулеру, который, как вы думаете, на вашей стороне, за определением:


                Data Binding
                A mechanism that ensures that any change made to the data in a UI control is automatically carried over to the underlying session state (and vice versa).

                Как видите, здесь лишь общие слова, и нигде не сказано, какую форму должен принимать data binding — {Binding ...}, source.subscribe(target) или же уродливый классический listener.


                Зато в определении сказано, что смысл Data Binding в том, что изменения сразу передаются из UI в состояние и наоборот. То есть изменяются синхронно, а состояние двух частей приложения, соответственно, синхронизировано, в чем и заключается основной профит привязывания данных.


                Таким образом, даже простой listener, который занимается синхронизацией двух значений, формально полностью подходит под это определение. Да даже без Фаулера сложно спорить с тем, что если вы заставили синхронно изменяться данные, то вы их связали.


                И кстати, из этого определения также следует, что автор статьи ошибочно противопоставляет binding при помощи Rx (частную реализацию) data binding'у в целом (общему понятию).


                Data Binding, помимо названия отдельных технологий, еще и абстракция. И в данной статье она применена.


                1. Jeevuz
                  25.04.2017 09:29

                  Вы пишете так много, что уже сами теряете нить.


                  Вопрос был о том, является ли связывание при помощи listener'а автоматическим.
                  Вы же ответили


                  Оповещает, конечно.

                  и зачем-то расписали свой ответ подробнее.


                  Суть в том, что есть 2 паттерна. Не один. Два. И мы сравнивая их видим отличие только в отношении связывания: "автоматическое" ли оно.


                  В нашем понимании в MVVM да, тк там фреймворк позволяет тебе не писать бойлерплейт код, а в PM нет, тк его приходится писать.


                  Связывание есть и там и там. Поэтому обсуждать databinding не вижу смысла.
                  Если хотите продолжать обсуждение, давайте вернемся к вопросу о понимании слова "автоматически".


                  Я выше написал, что понимаю под этим, а что вкладываете в это понятие вы?


                  1. deej
                    25.04.2017 13:39

                    Предыдущий комментарий был о том, что такое связывание. Отвечая, я исходил из того, что вы прочли первый комментарий, на который отвечали:


                    Может, data binding сам должен обновлять View? Связывание само не происходит, в том или ином виде оно всегда описано. Содержание остается тем же, а несущественная разница в форме не делает его "ручным" или "автоматическим".

                    То есть даже если за вас методы, инициирующие связывание, вызывает парсер разметки, вы все равно обязаны указать ему, что с чем связывать. Не укажете ничего — ничего и не свяжется "само".


                    Другими словами, я утверждаю, что связывание автоматически не происходит нигде. Автоматически изменяются значения зависимых свойств благодаря связыванию.


                    И это не просто мое мнение. Что Фаулер, что Microsoft в своих материалах применяют слово автоматический в ином контексте: при изменении некого значения другое должно автоматически меняться вслед за ним. И все, вот так банально. А data binding — это механизм, благодаря которому такое автоматическое изменение происходит. А те реализации, которые предоставляют android/winforms/wpf/angular/whatever, просто делают его кратким/удобным для использования (а иногда наоборот).


                    Причем, если смотреть на то, как красиво оно описано/количество boilerplate-кода, то с этим в настоящей статье все хорошо. Человек, знакомый с Rx, с легкостью опознает в строке


                    pm.loadingState.subscribe(progressBar.visibility())

                    привязку одного свойства к другому. Вам нужно еще более "автоматически"?


                    А вот вы на мой вопрос "Что для вас автоматическое, а что нет?" не ответили.


                    Я выше написал, что понимаю под этим

                    Либо я плохо смотрю. Хотя несколько раз просмотрел ваши комментарии, но ответа не увидел. Не могли бы вы написать еще раз?


                    1. Jeevuz
                      25.04.2017 18:06
                      +1

                      Мы тут спорим или общаемся?


                      "Описал выше" это прямо в том же комменте. Неужели не заметно было?
                      Вот:


                      Суть в том, что есть 2 паттерна. Не один. Два. И мы сравнивая их видим отличие только в отношении связывания: "автоматическое" ли оно.
                      В нашем понимании в MVVM да, тк там фреймворк позволяет тебе не писать бойлерплейт код, а в PM нет, тк его приходится писать.

                      Ответил? Тогда жду ответ на вопрос:


                      является ли связывание при помощи listener'а автоматическим.

                      Из ваших комментов получается, что связывание автоматическое всегда. И нигде не происходит автоматически. Тем самым вы отрицаете само существование паттерна PresentationModel и при этом еще и приводите Фаулера в доводы.


                      Вам нужно еще более "автоматически"?

                      Да, в Databinding Library и других это происходит еще более автоматически.


                      Теперь вернемся к Фаулеру:


                      Probably the most annoying part of Presentation Model is the synchronization between Presentation Model and view. It's simple code to write, but I always like to minimize this kind of boring repetitive code. Ideally some kind of framework could handle this, which I'm hoping will happen some day with technologies like .NET's data binding.

                      В статье же Джона Гроссмана, которую указал Mujahit, тот продолжает


                      WPF has done exactly that by including a very rich powerful data-binding engine. Basically, in the past all of these frameworks worked using the Observer or Publish/Subscribe pattern. In the simplest version the Model publishes change notifications and the View subscribes and updates itself in response to events. Not only is this code repetitive, but it can be a source of bugs and perf problems. The data binding engine in Avalon just automates all that work, and provides 2-way binding to boot that minimizes how much work you have to do to push changes from the View back into the Model.

                      Никаких знакомых слов не заметили? Выделю:


                      The data binding engine in Avalon just automates all that work

                      Еще немножечко сокращу:


                      automates all that work

                      Итак:


                      Мы понимаем различие между MVVM и PM как его понимают создатели этих паттернов.
                      В наличии автоматического databinding'a. Чего-то, что позволяеет упростить написание кода для связывания.


                      Вы же не станете отрицать, что написать в XML поле от которого зависит виджет проще, чем писать то, что приходится в приведенном нами паттерне?
                      Я отвечу — конечно проще.
                      Ведь, код, необходимый для связывания появится автоматически.


                      Исходя из всего сказанного, паттерн, описанный в статье, раз в нем надо писать код связывания самому и нет никакого фреймворка делающего за тебя эту работу, ближе к PM.


                      1. deej
                        26.04.2017 14:00
                        -1

                        Можно я буду использовать номера вместо цитирования? Надоели огромные комментарии и то, что разговор стоит на месте.


                        Автоматическое связывание в вашем понимании означает, что нет бойлерплейта? Все верно? Если да, то у меня для вас новость. В андроиде нужно либо оборачивать каждое поле в ObservableField<T>, либо сам класс модели должен наследовать интерфейс Observable, а в сеттерах должен вызываться метод, уведомляющий об изменениях. Аналогично и в WPF, вы используете либо DependencyProperty на каждое свойство, либо интерфейс INotifyPropertyChanged (и его родственников), превращая авто-свойства из одной строки в простыни однотипного текста. Всё перечисленное — boilerplate.


                        Уже одно это разбивает вашу предпосылку (та, что в цитате 1) в щепки.


                        Далее про "автоматическое Шрёдингера".
                        Вы называете автоматическим связыванием отсутствие boilerplate кода.
                        Автоматическое, значит без участия человека, совсем [1]. В отличии от автоматизированного, где требуется участие человека. Поэтому я и утверждаю, что его нет. Ведь во всех MVVM-фреймворках приходится писать что с чем связать, даже если мало. А как мы только что выяснили, еще и boilerplate нужен, так что его дважды нет.
                        А что происходит автоматически, так это изменение одних значений сразу после изменений других [2].
                        Так вот, [1] и [2] — это две разные вещи. Я догадывался по прошлым коментариям, что вы их не различаете, но теперь точно знаю.


                        Теперь про знакомые слова. Вы не видите смысла за отдельными словами. ".NET's data binding" — название конкретной технологии, а не databinding как самостоятельное понятие. "The data binding engine in Avalon" — оно же применительно конкретно к Avalon (WPF), т.е. еще более узко.
                        Здесь написано вовсе не "data binding делает работу автоматической". Здесь написано "вот это конкретная реализация от Microsoft автоматизирует всю работу". А вы почему-то решили из отдельных слов составить тот смысл, который вам будет удобен.
                        Обратите внимание еще и вот на что:


                        • Автоматизирует. Здесь говорится именно об этом. Автоматизированный это не автоматический.
                        • Прочтите выше про boilerplate в моделях, и поймете, что они погорячились насчет "all"

                        Ведь, код, необходимый для связывания появится автоматически

                        Неверно, не весь, см. про boilerplate.


                        Про краткость (в XML) не стану отрицать. Но где граница между "достаточно кратко" и "еще слишком многословно"? Кто должен это решать, и почему он? То, что эта граница субъективна по своей природе — еще одна причина, по которой "краткость записи" не может служить критерием для разделения между PM и MVVM.


                        Может, вообще не стоит различать PM и MVVM, а считать их разными именами одной сущности. Некоторые авторы так и делают. (То, что я пишу это, не значит, что ваши ошибки, на которые я указал выше, исчезли. Если вы над ними подумаете, это не сделает вас глупее или проигравшим).


                        В таком случае называть эту архитектуру MVVM также некорректно, как и говорить, что это PM.


                        Если вас устроит такой вывод, предлагаю завершить разговор. (Продолжить можно, но я устал от него)


                        Комментарий снова получился гигантским. Если я где-то в нем задел вас, прошу прощения, это неумышленно.


                        1. Jeevuz
                          26.04.2017 15:15

                          Я тоже догадывался о том, что вы говорите в основном о том, что где-то некорректно было применено слово автоматически. И я согласен, что корректнее будет использовать слово автоматизированно.


                          Про бойлерплейт, вы опять к словам цепляетесь. Никто не говорил про полное отсутствие. Его просто меньше.


                          Просто ведь если какой-то фреймворк "автоматизирует" работу, то значит какая-то часть работы происходит "автоматически". И тут начинается все заново. Поэтому пусть разбираются филологи и любители относительности. А спор на эту тему тут надо завершать.


                          Как называть данный паттерн думаю решать автору, тем более если мы согласились, что разница между PM и MVVM не значительна.


                          А вот этот выпад не очень корректен:


                          То, что я пишу это, не значит, что ваши ошибки, на которые я указал выше, исчезли. Если вы над ними подумаете, это не сделает вас глупее или проигравшим.
                          У вас ошибок было тоже полно. Так что говорить такое некрасиво.

                          Но "без обид".


                          Всего хорошего! Разговор завершен.


  1. lani
    20.04.2017 22:56

    А есть ли гитхаб репозиторий с исходниками этого приложения?


    1. dmdev
      20.04.2017 23:35

      Чуть позже выложу


      1. dmdev
        21.04.2017 02:29

        Залил примерчик на гитхаб https://github.com/dmdevgo/RxPM-Demo


    1. dimsmol
      21.04.2017 08:11
      +1

      кстати, хороший пример использования подобного подхода как для Android, так и для iOS – приложения Kickstarter, они открыли исходники не так давно:



      примеры особенны интересны тем, что это большое приложение целиком


  1. AlexeyKorshun
    20.04.2017 22:56
    +2

    Доброго дня, а есть пример на github.com?


    1. dmdev
      20.04.2017 23:36
      -1

      Ответил выше


    1. dmdev
      21.04.2017 12:38

      Залил примерчик на гитхаб https://github.com/dmdevgo/RxPM-Demo


  1. a_artikov
    21.04.2017 12:38

    Классная статья. Расскажите пожалуйста, как с этим подходом отображать ошибки, например, с помощью алерт-диалога. Чем в этом случае будет ошибка — стейтом или эвентом? Как правильно обработать скрытие ошибки пользователем?


    1. dmdev
      21.04.2017 13:55

      Все ошибки можно разделить на два типа:
      1) Ошибки, которые нужно показать один раз, например AlertDialog или Toast. В этом случае ошибка будет эвентом, ее сохранять не нужно. Для этого подойдет обычный PublishRelay. Но будет проблема, если ошибка прилетит в тот момент, когда вьюха отсоединена от PresentationModel. В этом случае мы потеряем этот эвент. Как решать эту проблему я расскажу в следующей статье.


      2) Ошибки, которые нужно показывать как заглушку в разметке, например с кнопкой "Retry", такой вариант нужно считать стейтом. Для этого нужно использовать BehaviorRelay.


  1. sargeras55
    21.04.2017 12:54

    Можно узнать, почему данный доклад не пропустили на Mobius?


    1. Jeevuz
      24.04.2017 09:04

      Судя по всему, тк он мог "конфликтовать" с докладом Степана Гончарова и Дениса Неклюдова.
      Хотя по-моему это не так. В их подходе используется обычный MVVM c Databinding Library, да и доклад был про подход к архитектуре в приложении в целом, а не только о презентационном паттерне. Поэтому, я думаю, конфликта не было бы. Но, что сделано, то сделано. Зато статья вышла быстрее ;)


  1. dm_bard
    22.04.2017 14:42

    Несколько вопросов/комментариев:

    • Соответственно, ссылка на интерфейс View в презентере будет обнулена. Поэтому нужно всегда делать проверку на null, когда требуется обновить View.
      Ссылку на интерфейс не обязательно обнулять. Можно подсунуть пустую реализацию интерфейса, тогда проверку делать не надо. В этом случае view будет val/@NonNull. Но даже если ссылка на вью будет nullable, при использовании котлина всегда можно заюзать Safe Call оператор ?.
    • На сколько данный подход поддается тестированию?
    • Зачем вам во фрагменте вот эта строчка
      retainInstance = true
      

      Тут и память может потечь, и в бэкстэк этот фрагмент не положить


    1. dmdev
      22.04.2017 20:59

      1. Не все пишут на Котлине, можно делать интерфейс-заглушку, но ее тоже придется генерировать. Но проблема не в этом. Так как вьюха может быт отсоединена, то приходится сохранять стейт в презентере в виде флагов, чтобы потом его воспроизвести при атаче вью.
      2. По поводу тестирования RxJava, есть хороший доклад на эту тему: https://www.youtube.com/watch?v=7W5NwpE5WpQ&feature=youtu.be
      3. Ретейн фрагмент только для семпла, так то для прода они не годятся. Как вы уже заметили в бекстеке такие фрагменты нельзя использовать и есть баги с чайлд-фрагментами. Я в своих приложениях использую Conductor — это такие "правильные" фрагменты, которые не умирают в бекстеке и при поворотах. А насчет памяти тут все в порядке, на onDestroyView мы отписываемся от PresentationModel.


  1. ZuZuK
    23.04.2017 00:57

    Интересно. Пользуюсь похожим подходом последние года 1.5, основные проблемы возникали именно с правильным хранением/изменением состояния и с навигацией.

    Насчет состояния — для экранов храню его в saveState, но, по-хорошему, его нельзя изменять после onSaveInstanceState, так что надо отписываться в правильных местах, грубо говоря.

    Насчет навигации, если делать на фрагментах/вьюхах — тоже нельзя ей пользоваться после onSaveInstanceState, иначе ее состояние не сохранится. Если на активити, то связать между собой 2 активити будет проблематично, если одна активити меняет состояние PM другой активити.

    В двух словах не ответите, как примерно решаете такие проблемы?


    1. dmdev
      23.04.2017 14:19
      +1

      Основная идея паттерна PM заключается в том, что стейт хранится в PresentationModel. View не нужно об этом беспокоиться и не нужно складывать стейт в Bundle. Главное реализовать хранение PresentationModel во время поворота.


      По поводу навигации, нужно складывать команды в буфер, и воспроизводить их когда навигатор (активити) будет готов. Посмотрите как это сделано в Cicerone.


      1. Xanderblinov
        26.04.2017 09:45
        +1

        Спасибо за интересную статью! dmdev а как подразумевается обработка ситуации, когда процесс убился и произошло последующее восстановление Activity?

        В этом случае у Вас PresentationModel будет в состоянии A а View в состоянии B


        1. dmdev
          26.04.2017 12:07
          +1

          Точно так же как и в Moxy ;)


          Рестарт процесса вещь неприятная, но не всегда требуется при этом восстанавливать View в то же самое состояние. Так как данные за время отсутствия пользователя в приложении могли устареть. Все зависит от конкретного приложения и в каждом случае нужно то или иное решение:


          1) Точно восстановится бэкстек из активити и фрагментов. При желании этот момент можно отследить и очистить бекстек, если приложение стартовало с восстановлением.


          2) Самые важные параметры экранов (параметры запуска) мы стараемся передавать через Intent или аргументы фрагмента, например id сущностей, которые нужно отобразить. PresentationModel получает их в конструкторе, так как View провайдит ее.


          3) Не все состояния нужно восстанавливать. Например прогресс загрузки не нужно восстанавливать, так как с убийством процесса все асинхронные запросы (в том числе и в сеть) тоже завершатся.


          4) Есть данные, которые быстро устаревают, например какие-нибудь статусы заказа. Лучше будет их заново запросить с сервера.


          5) Некоторые данные следует восстанавливать даже после принудительного завершения приложения. В этом случае никакие bundle нам не помогут. Например это может быть корзина с продуктами. Такие данные во время работы приложения нужно сохранять на диск (в бд или файл).


          6) Хорошо кешировать данные, которые не сильно теряют актуальность за относительно продолжительное время.


          7) Можно запустить сервис, чтобы повысить приоритет приложения в фоне. Тем самым снизить вероятность убийства процесса системой.


          8) В конце концов в PresentationModel можно пробрасывать вызовы сохранения/восстановления состояния из bundle. Но этот вариант не подходит для персистентных данных (пункт 5 и 6).


  1. dmdev
    26.04.2017 12:06

    .