Pull to refresh

Comments 59

Такое ощущение, что статью писал я :) Разве что команды, которая использует Moxy, по соседству нет. Впечатление от MVVM и data binding в проде уже идентичные, хотя используется всего месяц.

Поддержу, довольно толково и верно все описано, прям мысли читал)
MVVM заставляет работать с View одновременно двумя путями: через databinding и через методы View.

Я думаю, это касается не паттерна MVVM как такового, а скорее ограничений платформы на которой он применяется.
В статье об этом и речь, что на android'е MVVM не позволяет сделать все в пределах паттерна
Мне кажется в статье речь о том, что на андроиде люди MVP пытаются называть MVVM и страдают от результата.

Если у вас вьюмодель вынуждена знать о View — это не MVVM. Т.е. тут не «ограничения платформы, на которой он применяется», а «ограничения платфомы, из-за которых он не применяется».

И, кстати, тут «платфома» ≠ «android»

Ниже я ответил Sterk. Прочтите, плиз.
В статье я не говорю, что проблемы эти только с андроидом связаны. Есть общие, а есть андроидные.
Databinding Library не дает всех тех инструментов, что WPF.


А то, что VM знает о View всего лишь одно из решений. Можно сделать шину данных. VM тогда не будет знать ничего о View. Но это не решит описанных проблем.


И еще раз, это проблемы не в том, что что-то невозможно реализовать. Это проблемы с тем, что это не красиво.

Проблема в том, что это не имеет отношения к MVVM.

Вызов метода — это, в конечном счёте, посылка сообщения. Как и шина. То есть вы рассуждаете не про паттерн MVVM, а про технологию пересылки управляющих сигналов от презентера к вью.

Если вы весь обмен между вью и вьюмоделью повесить на шину — у вас не будет двойственности. Но отсутствие двойственности не помешает продолжать использовать MVP. MVVM от MVP отличается не тем, что биндинг в xml написан.

Не хочу спорить в пустую.


Давайте так:


  1. Дайте свое определение MVVM.
  2. Назовите сколько вы знаете систем, где биндинг настраивается не в XML.
  3. Назовите плюсы такого подхода.
  4. В системах с биндингом через XML, как запустить анимацию на View или отобразить данные используя системный механизм отложенного выполнения типа handler.post()?
  5. Как бы вы назвали то, что приходится и датабиндинг через xml писать и вызывать события иначе?
  6. Где в статье я говорю про MVP кроме явных мест?

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

> Как бы вы назвали то, что приходится и датабиндинг через xml писать и вызывать события иначе?

Вы со мной о чём-то своём хотите поспорить. Датабиндинг и xml — это «чем», паттерн — это «как». Из этого описания можно заключить, что об методах вью кто-то знает, значит это не MVVM. Судя по статье в целом — MVP.

Вы статью читали?
Речь о том, что сейчас много кто хочет использовать MVVM на андроиде. А автор статьи на собственном опыте показал, что на данный момент красиво и в рамках паттерна этого не сделать.
Приведены конкретные аргументы. Никто не принижает сам MVVM!
А вы все к словам придираетесь и хотите в другое русло загнуть.

У меня, после «Не думаем зачем, думаем как», окрепла уверенность, что человек не пытается использовать MVVM красиво и в рамках паттерна. Больше похоже на «ага! — сказали суровые сибирские мужики».

Если инструментарий не поощеряет MVVM — то можно и MVP, я не говорил, что он плох.Я даже не говорил, что MVVM хорош (там с «красиво» вообще не очень, честно говоря).

Нет, автор показал что в рамках используемой им библиотеки это красиво не сделать.

Не совсем. Скорее в рамках всех инструментов (которые я знаю на данный момент) это красиво сделать не удается. Во всяком случае, я пока не слышал о фреймворке, который решал бы красиво те проблемы, которые я описал.


Я бы и рад узнать о таком, потому что мне нравится сам паттерн MVVM.

Вы не хотите привести сперва ваше описание MVVM, но продолжаете утверждать, что в статье про MVP. Не отвечаете на другие мои вопросы. Похоже, что вы просто хотите поспорить. Я этого делать не хочу. Давайте на этом закончим.

Описание MVVM можно взять, например, там, где вы картинку от него взяли — https://msdn.microsoft.com/ru-ru/library/hh848246.aspx
Я использую MVVM при работе с WPF. Может вам будет это полезно.
MVVM заставляет работать с View одновременно двумя путями: через databinding и через методы View.
MVVM предполагает, что VM ничего не знает о View и соответственно не может использовать второй канал управления. Для того что бы передавать непосредственные команды во View используются различные интерфейсы, инъекция которых происходит в конструктор. Либо такие вещи как EventAggregator, Messenger, PubSub. И в первом и во втором случае реализация обработчиков происходит в code behind(не знаю как это по русски) во View. VM в свою очередь получает возможность абстрактно вызывать команды. Например в WPF элемент управления WebBrowser имеет метод Print, но напрямую вызвать его мы не можем. Поэтому при создание View содержащего WebBrowser мы реализовываем подписку на событие(через EventAggregator) печати в code behind. VM в любой момент может вызвать
eventAggregator.publish(new PrintEvent());
что соответственно запустит печать в контроле.
С MVVM нельзя красиво решить проблему состояний (необходимости сохранения вызова метода View, вызванного когда View была отсоединена от ViewModel).
VM как раз и должна быть отображением состояния вашего View. Сохранив VM вы как раз и сохраняете состояние View.

Не берусь утверждать, что все должно быть именно так, но такой подход позволяет решать задачи.
VM как раз и должна быть отображением состояния вашего View. Сохранив VM вы как раз и сохраняете состояние View.

+1
Рассматривайте вашу VM как адресную строку в гугл мапс — когда ваш друг открывает присланную вами ссылку, то он видит тоже самое, что и вы. То есть, по этой строке вы можете восстановить состояние приложения

А как тогда сохранить в VM то, что было показано через непосредственный вызов (например диалог)? Тут начинаются танцы с флагами и восстановлением состояния при ребиндинге вьюхи

Не очень понятно, что вы имеете ввиду.


Если вы про


Но что если в тот момент, когда фрагмент (View) отсоединён, фоновый процесс завершился с ошибкой, и мы хотим показать toast об этом? Фрагмент (выполняющий роль View) мёртв, и вызвать метод на нём нельзя.

То, я думаю, вам нужно куда-то складывать результаты ваших операций и при ребайнде (resurect, кажется, правильно называется), отображать ваши тосты.


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


Но тут возможно своя андроид-специфика; не знаю, что и когда у убивается там

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

Тут можно накостылить атрибуты на методы, открывающие диалог, добавить.


[OpenWhenResurectedIf("choosingPaymentCard")]
public void ShowChoosePaymentCardDialog() 
{
    this.State["choosingPaymentCard"] = true;
    // opening dialog, handle closing and then
    this.State["choosingPaymentCard"] = false;
}

Тут, кстати, можно ещё какой-нибудь АОП прикрутить, дописывающий установку состояний и новый фреймворк готов :)

"А можно взять Moxy", говорю я, как тот самый коллега ;-), но это уже не MVVM

Простите, но какой, простите, флаг? У вас больше одного диалога открыто в моменте?

Нормальный кейс — это последовательность открытых окон. Ну стек, в смысле, где "<" делает pop предыдущего. Не надо стек на флагах делать.
Я для примера привел диалог. Кто говорит о стеке?
Это может быть что угодно, Snackbar, например, или анимация какая-нибудь.
Как сохранить такие вещи статическим набором данных?
> Я для примера привел диалог. Кто говорит о стеке?

Ваш неудачный пример.

Что такое «анимация какая-нибудь» я не знаю, а Snackbar, на сколько я его понимаю, можно не сохранять. Если у вас «что-то вроде Snackbar», который нужно сохранять — скорее всего это не флаг, а вьюмодель снекбара.

Вопрос «как сериализовать дерево объектов» риторический, я надеюсь?
Не знаком с MVVM и с Databinding Library в частности, но похоже как раз вы сможете рассказать: как нам правильно и канонично показать toast или добавить новую View на экран, не выполняя её inflate заранее? И чтобы после пересоздания Activity/Fragment эта View оставалась на экране? Или для этого придётся отказаться от Databinding Library? Конечно без кода, а общими словами =)
Я сам сюда зашел как раз посмотреть, может тут знают как канонический MVVM на андроиде готовить. А тут та же боль, что и везде.

Сам я больше по WPF
А, я думал, что вы знаете правильный ответ. Видимо, я вас не так понял. Потому что статья автора как раз про эту боль, и он заранее предупреждает остальных =)

Мне кажется вы меня не совсем правильно поняли. Возможно я действительно привел неудачный пример. Извините!
Речь шла не о навигации и диалоговых окнах, а об особенности паттерна — только и всего.


Попробую еще раз: У нас на вьюхе могут быть состояния, которые нужно восстановить после поворта, но не связанные с самими данными.
Пример: экран с изображением. мы можем зуммить изображение и двигать его в стороны нажатием на кнопки "+", "-", "<-" и "->" соответственно. Обработкой нажатий занимается ViewModel.
В случае MVVM во ViewModel появятся дополнительные данные помимо самой картинки — это X, Y и зумм. Эти параметры я и назвал неудачно "флагами", так они тут только для сохранения состояния.
В случае подхода с сохранением очереди команд, рядом с самой картинкой не появится дополнительных "флагов", X, Y и зумм. Они будут неявно сохранены в очереди.

Скажите, а как эта задача решается без MVVM?

В MVP состояние View устанавливается вызовом набора команд на интерфейсе View. И если хранить этот стек вызвовов этих команд и применить их на вновь приаттаченой вью, то эта проблема решается. Но это для MVP красиво именно потому, что там используются методы для установки состояния вью, а не поля.

Парочка вопросов:

1) Есть ли смысл восстанавливать позицию, например, какой-нибудь скроллящейся вьюшки через презентер/вью модель? Можно ли оставить это в зоне ответственности самой вьюшки?

2) Предположим, по нажатию на кнопку надо плавно скроллить список в конец, пусть это будет реализовано через команду, при пересоздании вьюхи будет вызвана эта команда и список будет плавно прокручиваться в конец, вместо того чтобы сразу показывать нужную позицию. Как можно этого избежать?
1) По-хорошему этим должен заниматься презентер или вьюмодель. Например, хранить позицию выбранного элемента в списке. На практике это легче реализовать во вьюхе, но тогда вы не сможете протестировать восстановление состояния. Это та самая UI-логика, ради которой и создавались эти презентеры и вью-модели.

2) Нужно восстанавливать состояние вручную и проверять, что вьюха привязывается после восстановления. Запоминать команды с анимацией — плохая идея. Можно передавать команду скролла с анимацией, а в стейте сохранять без нее, только позицию.
С помощью EventAggregator/Messenger/PubSub вы можете реализовать вывод сообщения от любой VM в любой View. Например, мы получили ошибку. Мы вызываем (так же как в моем примере с печатью в WPF) отправку сообщения о событие. Любой кто хочет его обработать может это сделать. Это можно сделать как в базовом классе для всех View, так и в сервисе логирования. Нужно просто реализовать подписку на нужное событие. Этот способ устранит надобность хранения в принципе. Вы просто выведите сообщение на текущей View. Если же нужно выводить конкретно на данной View эти ошибки. То вы опять таки работаете только во VM — создаете список не отображенных ошибок. После события добавления View начинаете их выводить. Отмечу, так пробовал бы действовать я, но опыта android на java у меня нет.

Во первых, спасибо Sterk за описание того, как устрокна работа на WPF. Я думаю это самая презентабельная система в плане MVVM.


Но для отдельных действий нам надо все же приходится писать code behind и вызывать его как-то. Не важно как, через eventAggregator (который как я понимаю представляет собой шину данных), либо напрямую через интерфейс вью (да, я понимаю это подход ближе MVP). Суть одна и та же.
А раз нам, помимо того, что мы биндим данные автоматом, надо как-то вызывать что-то руками (кидать события в шину данных, писать код их обработки во View), то возникает то, что я назвал "проблема двойственности". И я согласен, с этим можно жить. Но лично мне это не нравится.


… создаете список не отображенных ошибок. После события добавления View начинаете их выводить.

Согласен, так я и описал как можно это делать. Но это мне тоже не нравится. Так как приходится не просто сохранить VM как состояние View, а добавлять дополнительную логику (тот самый вызов списка ошибок) и поля (сам список ошибок).


И немного философии: Мне в MVVM как раз нравится то, что VM это набор полей отображающих состояние View. И когда надо добавлять в нее методы, тоже воссоздающие состояние, то красота и чистота VM пропадает.

Не вижу никакой "проблемы двойственности" в обработке событий.


Красота MVVM тоже никак не страдает, если писать логику отображения списка ошибок там, где ей самое место — во View. И не забывать про принцип DRY.

Спорный момент. Логика отображения — это как раз то, что должно быть в VM. Все эти паттерны созданы для выноса логики отображения из View, чтобы можно было тестировать и заменять View. А раз логика обработки ошибок попала во View, то захотев подменить View на другую, нам придется в другой прописывать ту же логику обработки ошибок.
И чтобы не путаться, уточню, что я говорю про логику, а не про визуальный элемент, отображающий ошибку.
Да и кроме ошибок есть еще и системные вещи, типа задержек и тп. К примеру, в одном случае я хочу вывести текст с задежкой в 2 секунды, а в другой 3, и тд. Получается, что это попадет во вью? Вью будет содержать по обработчику для каждой ситуации? Или в обработчике будет параметр?
Тогда это похоже на метод. А тогда мы приходим к тому, что надо вызывать его. И отсюда к проблеме двойственности. Вызов метода и автодатабиндинг.

Что вы понимаете под "логикой обработки ошибок"? Логика тут простая: каждая ошибка должна быть показана пользователю в течении некоторого времени ровно 1 раз.


Во View не будет ни обработчика для каждой ситуации, ни метода с параметром. View должна отслеживать список сообщений, каждое сообщение будет содержать длительность отображения. Показали — убрали из списка, взяли следующее. Когда список пуст — подписываемся на событие его изменения.


Это тот же "датабиндинг", только без приставки "авто".

Да, но без приставки "авто" это уже паттерн Presentation Model. И как раз к нему у меня претензий вообще нет ;)


А так, я с вами не спорю. В статье я вроде пару раз сказал, что выводы носят отчасти субъективный характер. То как приходится работать мне не по душе. К этому добавляются еще и ограничения платформы и библиотеки.

Что-то мешает использовать в проекте два разных паттерна для разных ситуаций?..

Кстати, по поводу WPF. За 10 лет его использования я пришёл к выводу, что там нужно использовать только MVVM. Более того, WPF «из коробки» производит впечатление, что там не хватает такой важной части как, например, Caliburn.Micro.

Когда два года назад я занялся разработкой под Android, первое, что я сделал — начал искать подходящий фреймворк, реализующий MVVM. Выбрал RoboBinding, провозился с ним несколько дней, натыкаясь на проблемы и костыли. В итоге вернулся к старому доброму findViewById, использую такой подход до сих пор и ни на что не жалуюсь:)
" За 10 лет его использования я пришёл к выводу, что там нужно использовать только MVVM. Более того, WPF «из коробки» производит впечатление, что там не хватает такой важной части как, например, Caliburn.Micro."
Больше того, о MVVM пишут во всех официальных гайдах. Но при этом нельзя использовать MVVM + WPF пользуясь стандартными инструментами, приходится писать свои дополнения.
Спасибо за статью, тема MVVM на андроид очень интересна. Сам стараюсь придерживаться этого подхода в меру своего понимания. К сожалению, мне так и не удалось найти каноничный пример этой технологии. То что предлагает Google тут, выглядит сильно усложненным

Мне кажется, как минимум одна проблема у Вас надумана. Вы пишете

Можно также обернуть поля в ObservableField, и зависимые элементы UI будут обновляться автоматически. Но я считаю такой способ менее гибким и редко его использую.


Потом приводите пример, где Вам нужно писать boilerplate с notifyPropertyChanged

public void setUser(User user) {
        name = user.firstname + user.lastname;
        notifyPropertyChanged(BR.name);
        notifyPropertyChanged(BR.usePost);
        usePost = false;
    }



Ведь будь у Вас все завернуто в ObservableField, нужно было бы только написать
public void setUser(User user) {
        name.set(user.firstname + user.lastname);
        usePost.set(false)
    }

И избавиться от bindable геттеров

Также к ObservableField легко подключить RxJava и в несколько строчек делать всевозможные связи между полями (когда изменение одного поля, вызывает цепочку изменений других полей).
Из жизненных примеров выгоды подключения RxJava к ObservableField

Например, у модели есть булевое observable поле loading, которое контролирует отображение индикатора загрузки.

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

Тогда вступает в дело RxJava

Observable.combineLatest(loading1.asObservable(), ..., loadingN.asObservable(),
(loading1, ..., loadingN) -> loading1 || .. || loadingN)
.subscribe(loading.asAction())


Конечно, этот функционал можно написать и без Rx и без DataBinding, но кода будет явно побольше.

Спасибо за коммент. А я про проблему с тем, что нужно писать boilerlate не используя ObservableField и не писал ;)


В простых случаях можно использовать ObservableFields, да. Но когда надо сделать связи между полями (где вы предлагаете использовать Rx), можно наследоваться от BaseObservable и настроить связи там.
А чтобы не делать в одном случае так, в другом так я предпочитаю чаще использовать второй вариант.

В MVVM фреймворке Prism есть возможность использовать IInteractionRequest — такой запрос VM к V на взаимодействие с пользователем. Можно применять, как для вывода простого диалогового окна, так и для открытия новой формы с кучей полей и своей VM.
В приведённых библиотеках есть такие вещи, как триггеры? Такой функционал, как я описал выше, реализуется с их помощью.

Спасибо за комментарий с информацией!
Нет, в Databinding Library нет триггеров. Но это не сложно реализовать самим.
В любом случае это мне и не нравится, что кроме автобиндинга приходится использовать запросы от VM к V.

Если вы про InteractionRequest, то это не запросы к V. Это просто абстрактные запросы во внешний мир. Их могут отработать к виюхи, так и моки в автотестах

Можно обойтись ненамного большим объемом кода, если сразу пойти от одного свойства loading.


int loadingCount;

@Bindable 
bool getLoading() {
  return loadingCount > 0;
}

AutoCloseable beginLoad() {
  loadingCount++;
  notifyPropertyChanged(BR.loading);
  return () -> {
    loadingCount--;
    notifyPropertyChanged(BR.loading);
  };
}

Примерно так, если я ничего не напутал. На java давно уже не пишу, а для Андроида — даже никогда не писал, так что не пинайте сильно если что перепутал.

Да, это будет работать. Но что, если поля loading1, ..., loadingN находятся в других (разных) классах? Получается надо делать цепочку вызовов наверх, чтобы вызвать метод beginLoad
Спасибо, тоже вариант. Rx ведь не единственно верный путь, просто помогает уменьшить количество boilerplate кода
А каким образом Вы сохранили ViewModel при повороте экрана от уничтожения, используя Dagger Scope? Насколько я знаю, Scope на самом деле не добавляет никакой логики при инжекции. Оно как бы «логически» определяет, что данный Component будет жить столько же, сколько Scope.
Т.е., создав Component внутри Fragment, при повороте экрана Component также будет уничтожен и затем пересоздан. Единственный способ — хранить его отдельно, в каком-нибудь Синглтоне. Но в таком случае можно не заморачиваться и хранить в Синглтоне сразу ViewModel.
Или я чего-то о Dagger не знаю? Перечитал документацию по нему — не нашел способа сохранять Component при поворотах экрана.

Так в этом и идея, что при повороте можно это определить во фрагменте и не очищать Dagger Scope, в остальных случаях он остается в памяти:


@Override
public void onDestroy() {
  if (isRemoving() || getActivity().isFinishing() {
    //здесь очищаем Scope
  }
}
«логически» определяет, что данный Component будет жить столько же, сколько Scope.

Да, умирает компонент и вместе с ним все, что он в себе содержал. Все элементы отмеченные scope.


Т.е., создав Component внутри Fragment, при повороте экрана Component также будет уничтожен

Конечно. Сам component не во фрагменте. Обычно хранят в Application или можно в своем синглтоне. Как нравится.


Но в таком случае можно не заморачиваться и хранить в Синглтоне сразу ViewModel.

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


В общем схема примерно такая: в Application лежит component, в котором лежит VM. Когда происходит поворот мы проверяем поворот ли это и если да, не делаем ничего. Если это не поворот, а фрагмент умирает, то обнуляем component. И в следующий раз получаем уже новую VM из нового component.
Это я описал грубо, для общего понимания схемы. В реальности у нас много фрагментов и много VM, поэтому убивать весь component плохо. И поэтому в компоненте хранится образно мапа вьюмоделей, и обнуляется VM в ней когда умирает фрагмент.

Only those users with full accounts are able to leave comments. Log in, please.