Комментарии 60
Такое ощущение, что статью писал я :) Разве что команды, которая использует Moxy, по соседству нет. Впечатление от MVVM и data binding в проде уже идентичные, хотя используется всего месяц.
MVVM заставляет работать с View одновременно двумя путями: через databinding и через методы View.
Я думаю, это касается не паттерна MVVM как такового, а скорее ограничений платформы на которой он применяется.
Если у вас вьюмодель вынуждена знать о View — это не MVVM. Т.е. тут не «ограничения платформы, на которой он применяется», а «ограничения платфомы, из-за которых он не применяется».
И, кстати, тут «платфома» ≠ «android»
Ниже я ответил Sterk. Прочтите, плиз.
В статье я не говорю, что проблемы эти только с андроидом связаны. Есть общие, а есть андроидные.
Databinding Library не дает всех тех инструментов, что WPF.
А то, что VM знает о View всего лишь одно из решений. Можно сделать шину данных. VM тогда не будет знать ничего о View. Но это не решит описанных проблем.
И еще раз, это проблемы не в том, что что-то невозможно реализовать. Это проблемы с тем, что это не красиво.
Вызов метода — это, в конечном счёте, посылка сообщения. Как и шина. То есть вы рассуждаете не про паттерн MVVM, а про технологию пересылки управляющих сигналов от презентера к вью.
Если вы весь обмен между вью и вьюмоделью повесить на шину — у вас не будет двойственности. Но отсутствие двойственности не помешает продолжать использовать MVP. MVVM от MVP отличается не тем, что биндинг в xml написан.
Не хочу спорить в пустую.
Давайте так:
- Дайте свое определение MVVM.
- Назовите сколько вы знаете систем, где биндинг настраивается не в XML.
- Назовите плюсы такого подхода.
- В системах с биндингом через XML, как запустить анимацию на View или отобразить данные используя системный механизм отложенного выполнения типа handler.post()?
- Как бы вы назвали то, что приходится и датабиндинг через xml писать и вызывать события иначе?
- Где в статье я говорю про MVP кроме явных мест?
Просто иначе получается какой-то разговор ни о чем, уж простите.
Я рад обсуждать, но что мы обсуждаем мне не ясно.
Вы со мной о чём-то своём хотите поспорить. Датабиндинг и xml — это «чем», паттерн — это «как». Из этого описания можно заключить, что об методах вью кто-то знает, значит это не MVVM. Судя по статье в целом — MVP.
Вы статью читали?
Речь о том, что сейчас много кто хочет использовать MVVM на андроиде. А автор статьи на собственном опыте показал, что на данный момент красиво и в рамках паттерна этого не сделать.
Приведены конкретные аргументы. Никто не принижает сам MVVM!
А вы все к словам придираетесь и хотите в другое русло загнуть.
Если инструментарий не поощеряет MVVM — то можно и MVP, я не говорил, что он плох.Я даже не говорил, что MVVM хорош (там с «красиво» вообще не очень, честно говоря).
Нет, автор показал что в рамках используемой им библиотеки это красиво не сделать.
Вы не хотите привести сперва ваше описание MVVM, но продолжаете утверждать, что в статье про MVP. Не отвечаете на другие мои вопросы. Похоже, что вы просто хотите поспорить. Я этого делать не хочу. Давайте на этом закончим.
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 как адресную строку в гугл мапс — когда ваш друг открывает присланную вами ссылку, то он видит тоже самое, что и вы. То есть, по этой строке вы можете восстановить состояние приложения
Не очень понятно, что вы имеете ввиду.
Если вы про
Но что если в тот момент, когда фрагмент (View) отсоединён, фоновый процесс завершился с ошибкой, и мы хотим показать toast об этом? Фрагмент (выполняющий роль View) мёртв, и вызвать метод на нём нельзя.
То, я думаю, вам нужно куда-то складывать результаты ваших операций и при ребайнде (resurect, кажется, правильно называется), отображать ваши тосты.
Если вы про то, что в момент убийства вьюхи, был открыт диалог, то его тоже можно заново отображать. Ну да, здесь будет нужен отдельный флаг. Для диалога, соответственно, своя VM
Но тут возможно своя андроид-специфика; не знаю, что и когда у убивается там
Тут можно накостылить атрибуты на методы, открывающие диалог, добавить.
[OpenWhenResurectedIf("choosingPaymentCard")]
public void ShowChoosePaymentCardDialog()
{
this.State["choosingPaymentCard"] = true;
// opening dialog, handle closing and then
this.State["choosingPaymentCard"] = false;
}
Нормальный кейс — это последовательность открытых окон. Ну стек, в смысле, где "<" делает pop предыдущего. Не надо стек на флагах делать.
Это может быть что угодно, Snackbar, например, или анимация какая-нибудь.
Как сохранить такие вещи статическим набором данных?
Ваш неудачный пример.
Что такое «анимация какая-нибудь» я не знаю, а Snackbar, на сколько я его понимаю, можно не сохранять. Если у вас «что-то вроде Snackbar», который нужно сохранять — скорее всего это не флаг, а вьюмодель снекбара.
Вопрос «как сериализовать дерево объектов» риторический, я надеюсь?
Сам я больше по WPF
Мне кажется вы меня не совсем правильно поняли. Возможно я действительно привел неудачный пример. Извините!
Речь шла не о навигации и диалоговых окнах, а об особенности паттерна — только и всего.
Попробую еще раз: У нас на вьюхе могут быть состояния, которые нужно восстановить после поворта, но не связанные с самими данными.
Пример: экран с изображением. мы можем зуммить изображение и двигать его в стороны нажатием на кнопки "+", "-", "<-" и "->" соответственно. Обработкой нажатий занимается ViewModel.
В случае MVVM во ViewModel появятся дополнительные данные помимо самой картинки — это X, Y и зумм. Эти параметры я и назвал неудачно "флагами", так они тут только для сохранения состояния.
В случае подхода с сохранением очереди команд, рядом с самой картинкой не появится дополнительных "флагов", X, Y и зумм. Они будут неявно сохранены в очереди.
Скажите, а как эта задача решается без MVVM?
1) Есть ли смысл восстанавливать позицию, например, какой-нибудь скроллящейся вьюшки через презентер/вью модель? Можно ли оставить это в зоне ответственности самой вьюшки?
2) Предположим, по нажатию на кнопку надо плавно скроллить список в конец, пусть это будет реализовано через команду, при пересоздании вьюхи будет вызвана эта команда и список будет плавно прокручиваться в конец, вместо того чтобы сразу показывать нужную позицию. Как можно этого избежать?
2) Нужно восстанавливать состояние вручную и проверять, что вьюха привязывается после восстановления. Запоминать команды с анимацией — плохая идея. Можно передавать команду скролла с анимацией, а в стейте сохранять без нее, только позицию.
Во первых, спасибо 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. И как раз к нему у меня претензий вообще нет ;)
А так, я с вами не спорю. В статье я вроде пару раз сказал, что выводы носят отчасти субъективный характер. То как приходится работать мне не по душе. К этому добавляются еще и ограничения платформы и библиотеки.
Когда два года назад я занялся разработкой под Android, первое, что я сделал — начал искать подходящий фреймворк, реализующий MVVM. Выбрал RoboBinding, провозился с ним несколько дней, натыкаясь на проблемы и костыли. В итоге вернулся к старому доброму findViewById, использую такой подход до сих пор и ни на что не жалуюсь:)
Больше того, о MVVM пишут во всех официальных гайдах. Но при этом нельзя использовать MVVM + WPF пользуясь стандартными инструментами, приходится писать свои дополнения.
Мне кажется, как минимум одна проблема у Вас надумана. Вы пишете
Можно также обернуть поля в 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 и в несколько строчек делать всевозможные связи между полями (когда изменение одного поля, вызывает цепочку изменений других полей).
Например, у модели есть булевое observable поле loading, которое контролирует отображение индикатора загрузки.
Допустим, у нас есть много различных операций, требующих отображения индикатора. Индикатор также должен быть видимым, если хотя бы одна из операций активна
Тогда вступает в дело RxJava
Observable.combineLatest(loading1.asObservable(), ..., loadingN.asObservable(),
(loading1, ..., loadingN) -> loading1 || .. || loadingN)
.subscribe(loading.asAction())
Конечно, этот функционал можно написать и без Rx и без DataBinding, но кода будет явно побольше.
Спасибо за коммент. А я про проблему с тем, что нужно писать boilerlate не используя ObservableField и не писал ;)
В простых случаях можно использовать ObservableFields, да. Но когда надо сделать связи между полями (где вы предлагаете использовать Rx), можно наследоваться от BaseObservable и настроить связи там.
А чтобы не делать в одном случае так, в другом так я предпочитаю чаще использовать второй вариант.
В приведённых библиотеках есть такие вещи, как триггеры? Такой функционал, как я описал выше, реализуется с их помощью.
Спасибо за комментарий с информацией!
Нет, в Databinding Library нет триггеров. Но это не сложно реализовать самим.
В любом случае это мне и не нравится, что кроме автобиндинга приходится использовать запросы от VM к V.
Почему — "кроме"? Эти запросы через автобиндинг и проходят: https://habrahabr.ru/post/152003/
Можно обойтись ненамного большим объемом кода, если сразу пойти от одного свойства loading.
int loadingCount;
@Bindable
bool getLoading() {
return loadingCount > 0;
}
AutoCloseable beginLoad() {
loadingCount++;
notifyPropertyChanged(BR.loading);
return () -> {
loadingCount--;
notifyPropertyChanged(BR.loading);
};
}
Примерно так, если я ничего не напутал. На java давно уже не пишу, а для Андроида — даже никогда не писал, так что не пинайте сильно если что перепутал.
Т.е., создав 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 в ней когда умирает фрагмент.
Отказался от databinding с самого начала (2018 год). Удивительно, что оно так зашло людям в 2016 году. Библиотека ведь ужасная! Одни костыли - сшили ужа с ежом. Чуть что посложнее пытаешься сделать и никто это уже не поймет - проще заново переписать, чем разбираться в хитросплетенья и форматах. Брррр
Как перестать использовать MVVM