Двухголовый MVVM

На недавнем DroidCon Moscow 2016 был доклад о MVVM c Databinding Library и доклад о библиотеке Moxy, помогающей работать с MVP. Дело в том, что за последние полгода мы успели опробовать оба подхода на живых проектах. И я хочу рассказать о своём пути от освоения Databinding Library и выпуска в продакшн проекта на MVVM до осознания, почему я больше не хочу использовать этот паттерн.


Посвящается всем, кого зацепила Databinding Library и кто решил строить приложение на MVVM, – вы отважные люди!

Databinding Library


Начав разбираться с Databinding Library, я был под впечатлением. Те, кто уже знаком с ней, меня поймут, а для остальных, вот как выглядит работа с этой библиотекой:



Использование Databinding Library позволяет:


  • Избавиться от вызовов findViewById и setOnClickListener. То есть, указав id в xml, можно обращаться к view через binding.viewId. И можно устанавливать вызовы методов прямо из xml;
  • Связать данные напрямую с элементами view. Мы вызываем binding.setUser(user), а в xml указываем, к примеру, android:text = “@{user.name}”;
  • Создавать кастомные атрибуты. Например, если мы хотим загружать изображения в ImageView при помощи библиотеки Picasso, то можем создать BindingAdapter для атрибута “imageUrl”, а в xml писать bind:url=”@{user.avatarUrl}”.
    Такой BindingAdapter будет выглядеть так:
    @BindingAdapter("bind:imageUrl")
    public static void loadImage(ImageView view, String url) {
       Picasso.with(view.getContext()).load(url).into(view);
    }
  • Cделать состояние view зависимым от данных. Например, отображается ли индикатор загрузки, будет зависеть от того, есть ли данные.

Последний пункт особенно приятен для меня потому, что состояния всегда были сложной темой. Если на экране нужно отобразить три состояния (загрузка, данные, ошибка), это ещё ладно. Но, когда появляются различные требования к состоянию элементов в зависимости от данных (например, отображать текст только если он не пустой, или менять цвет в зависимости от значения), может понадобиться либо большой switch cо всеми возможными вариантами состояний интерфейса, либо много флагов и кода в методах установки значений элементам.
Поэтому то, что Databinding Library позволяет упростить работу с состояниями, – огромный плюс. К примеру, написав в xml android:visibility=”@{user.name != null ? View.VISIBLE : View.GONE}”, мы можем больше не думать о том, когда надо скрыть или показать TextView с именем пользователя. Мы просто задаём имя, а видимость изменится автоматически.


ViewModel


Но, начав использовать databinding активнее, вы получите в xml всё больше и больше кода. И, чтобы не превращать layout в свалку, мы создадим класс, в который вынесем этот код. А в xml будут оставаться только вызовы свойств. Приведу маленький пример. Предположим, есть класс User:


public class User {
    public firstname;
    public lastname;
}

А в UI мы хотим видеть полное имя и пишем в xml:


<TextView
    android:text="@{user.firstname + user.lastname}"
    />

Это не очень хочется видеть в xml, и мы создаём класс, в который выносим эту логику:


public class UserViewModel extends BaseObservable {

    private String name;

    @Bindable
    public String getFullname() {
        return name;
    }

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

Создатели библиотеки предлагают называть такие классы ViewModel (прям, как в паттерне MVVM, удивительно).


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

Теперь в xml у нас будет:


<TextView
    android:text="@{viewmodel.name}"
    />

Гораздо лучше, не правда ли?


Итак, у нас появился ViewModel класс, который выступает в роли прослойки между данными и view. Он занимается преобразованиями данных, управляет тем, какие поля (и связанные элементы UI) и когда обновляются, содержит логику того, как одни поля зависят от других. Это позволяет очистить xml от кода. Кроме того, удобно использовать этот класс для обработки событий из view (нажатия и т.п).


И тут к нам приходит мысль: Если у нас уже есть databinding, есть ViewModel класс, содержащий логику отображения, то почему бы не использовать паттерн MVVM?


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


MVVM


В паттерне Model-View-ViewModel три основных компонента:


  • Model. Бизнес-логика приложения, предоставляющая данные для отображения.
  • View. Отвечает за внешний вид, расположение и структуру всех UI-элементов, которые пользователь видит на экране.
  • ViewModel. Выступает мостом между View и Model и обрабатывает логику отображения. Запрашивает у Model данные и передает их View в виде, который View может легко использовать. Также содержит обработку событий, совершенных пользователем приложения во View, таких, как нажатие на кнопку. Кроме того, ViewModel отвечает за определение дополнительных состояний View, которые надо отображать, например, идет ли загрузка.

Связь и взаимодействие между собой этих компонентов мы видим на картинке:



Стрелками показаны зависимости: View знает о ViewModel, а ViewModel знает о Model, но модель ничего не знает о ViewModel, которая ничего не знает о View.


Процесс такой: ViewModel запрашивает данные у Model и обновляет её когда необходимо. Model уведомляет ViewModel, что данные есть. ViewModel берёт данные, преобразует их и уведомляет View, что данные для UI готовы. Связь между ViewModel и View осуществляется путём автоматического связывания данных и отображения. В нашем случае это достигается через использование Databinding Library. При помощи databinding’а View обновляется, используя данные из ViewModel.


Наличие автоматического связывания (databinding) является главным отличием этого паттерна от паттерна PresentationModel и MVP (в MVP Presenter изменяет View путём вызова на ней методов через предоставленный интерфейс).

MVVM в Android


Так я начал использовать MVVM в своем проекте. Но, как часто бывает в программировании, теория и практика – не одно и тоже. И после завершения проекта у меня осталось чувство неудовлетворенности. Что-то было не так в этом подходе, что-то не нравилось, но я не мог понять, что именно.


Тогда я решил нарисовать схему MVVM на Android:



Рассмотрим, что в итоге получается:


ViewModel содержит поля, используемые в xml для биндинга данных (android:text=”@{viewmodel.username}”), обрабатывает события вызванные на View (android:onClick=”@{viewmodel::buttonClicked}”). Она запрашивает данные у Model, преобразует их, и при помощи databinding’a эти данные попадают во View.


Fragment одновременно выполняет две роли: входная точка, обеспечивающая инициализацию и связь с системой, и View.


То, что Fragment (или Activity) рассматриваются как View в понимании паттернов MVP и MVVM, уже стало распространённой практикой, поэтому я не стану на этом останавливаться.


Чтобы пережить повороты и пересоздание Activity, мы оставляем ViewModel жить на то время, пока пересоздаётся View (в нашем случае Fragment). Достигается это с использованием dagger и пользовательских scopes. Не стану вдаваться в подробности, уже написано много хороших статей про dagger. Своими словами, происходит следующее:


  • ViewModel создается при помощи dagger (и её инстанс живёт в нём), и фрагмент берет её когда нужно.
  • Когда фрагмент умирает при повороте, он вызывает detachView() у ViewModel.
  • ViewModel продолжает жить, её фоновые процессы тоже, и это очень удобно.
  • Потом, когда фрагмент пересоздан, он вызывает attachView() и передаёт себя в качестве View (используя интерфейс).
  • Если же фрагмент умирает полностью, а не из-за поворота, то он убивает scope (обнуляется нужный компонент dagger, и ViewModel может быть собрана garbage collector’ом вместе с этим компонентом) и ViewModel умирает. Это реализовано в BaseFragment.

Зачем фрагмент передаёт себя во ViewModel, используя интерфейс MvvmView? Это нужно для того, чтобы мы могли вызывать команды «вручную» на View. Не всё можно сделать при помощи Databinding Library.

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


Примерно так всё работает.


Внимательный читатель спросит: «A чего мучаться с dagger custom scopes, если можно просто использовать Fragment как контейнер и вызвать в нём setRetainInstance(true)?» Да, так сделать можно. Но, рисуя схему, я учитывал, что в качестве View можно использовать Activity или ViewGroup.


Недавно я нашел хороший пример реализации MVVM, полностью отражающий нарисованную мной структуру. За исключением пары нюансов, всё сделано очень хорошо. Посмотрите, если интересно.

Проблема двойственности


Нарисовав схему и обдумав всё, я понял, что именно меня не устраивало во время работы с этим подходом. Взгляните на схему снова. Видите толстые стрелки «databinding» и «manual commands to view»? Вот оно. Сейчас расскажу подробнее.


Раз у нас есть databinding, то большую часть данных мы можем просто устанавливать в View при помощи xml (создав нужный BindingAdapter, если понадобится). Но есть случаи, которые не укладываются в этот подход. К таким относятся диалоги, toast’ы, анимации, действия с задержкой и другие сложные действия с элементами View.


Вспомним пример с TextView:


<TextView
    android:text="@{viewmodel.name}"
    />

Что, если нам нужно установить этот текст, используя view.post(new Runnable())? (Не думаем зачем, думаем как)


Можно сделать BindingAdapter, в котором создать атрибут «byPost», и сделать, чтобы учитывалось наличие перечисленных атрибутов у элемента.


@BindingAdapter(value = {"text", "byPost"}, requireAll = true)
public static void setTextByPost(TextView textView, String text, boolean byPost) {
   if (byPost) {
       textView.post(new Runnable {
           public void run () {
               textView.setText(text);
           }
       })
   } else {
       textView.setText(text);
   }
}

И теперь каждый раз, когда у TextView будут указаны оба атрибута, будет использоваться этот BindingAdapter. Добавим атрибут в xml:


<TextView
    android:text="@{viewmodel.name}"
    bind:byPost="@{viewmodel.usePost}"
    />

ViewModel теперь должно иметь свойство, указывающее на то, что в момент установки значения мы должны использовать view.post(). Добавим его:


public class UserViewModel extends BaseObservable {

    private String name;
    private boolean usePost = true; // only first time

    @Bindable
    public String getFullname() {
        return name;
    }

    @Bindable
    public boolean getUsePost() {
        return usePost;
    }

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

Видите, сколько всего нужно сделать, чтобы реализовать очень даже простое действие?


Поэтому гораздо проще делать подобные вещи прямо на View. То есть использовать интерфейс MvvmView, который реализуется нашим фрагментом, и вызывать методы View (так же, как это обычно делается в MVP).


Вот тут и проявляется проблема двойственности: мы работаем с View двумя разными способами. Один – автоматический (через состояние данных), второй – ручной (через вызовы команд на view). Лично мне это не по душе.

Проблема состояний


Теперь расскажу о ещё одной проблеме. Представим ситуацию с поворотом телефона.


  1. Мы запустили приложение. ViewModel и View (фрагмент) живы.
  2. Повернули телефон – фрагмент умер, а ViewModel живёт. Все её фоновые задачи продолжают работать.
  3. Новый фрагмент создался, присоединился. View через databinding получила сохраненное состояние (поля) из ViewModel. Всё круто.
  4. Но что если в тот момент, когда фрагмент (View) отсоединён, фоновый процесс завершился с ошибкой, и мы хотим показать toast об этом? Фрагмент (выполняющий роль View) мёртв, и вызвать метод на нём нельзя.
  5. Мы потеряем этот результат.

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


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


Про состояния


Проблема состояний натолкнула меня на мысли, что состояние объекта можно воссоздать двумя путями: набором параметров, характеризующим состояние, или набором действий, которые необходимо совершить, чтобы привести объект в нужное состояние.


Представьте себе кубик Рубика. Его состояние можно описать 9 цветами на одной из граней. А можно набором движений, которые приведут его из начального состояния в требуемое.



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


Moxy


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


В контексте моих рассуждений интересна одна особенность Moxy – она хранит состояние view как набор команд, вызванных на этой view. И, когда я узнал об этом впервые, мне это показалось странным.


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


  • Не всегда можно (удобно) представить состояние только данными (полями).
  • В MVP общение с View идёт через вызовы команд. Почему бы это не использовать?
  • В реальности количество полей view, нужных, чтобы воссоздать ее состояние, может быть куда больше числа вызванных на ней команд.

Кроме того, этот подход даёт ещё один плюс. Он также, как и Databinding Library, по-своему решает проблему большого количества разных состояний. Тоже не придется писать огромный switch, изменяющий UI в зависимости от набора полей или названия одного из состояний, так как изменения воссоздаются набором вызовов методов.


И всё же я не могу совсем ничего больше не сказать про Moxy. По моему мнению и мнению моих коллег, на сегодняшний день она является лучшей библиотекой, которая помогает наладить работу с паттерном MVP. Она использует генерацию кода, чтобы минимизировать трудозатраты разработчика. Вы можете не думать о реализации паттерна, а думать о функционале своего проекта. А это хорошо.


Но хватит про MVP. Всё-таки речь у нас про MVVM, и пора подвести итоги.


Выводы


Мне нравится MVVM как паттерн, и я не оспариваю его плюсы. Но в большинстве своём они те же самые, что у других паттернов, либо являются делом вкуса разработчика. Да и основной плюс даёт всё же databinding, а не сам паттерн.


Ведомый симпатией к MVVM, я реализовал проект на нём. Долго изучал тему, обдумывал, обсуждал и вынес для себя набор минусов этого паттерна:


  • MVVM заставляет работать с View одновременно двумя путями: через databinding и через методы View.
  • С MVVM нельзя красиво решить проблему состояний (необходимости сохранения вызова метода View, вызванного когда View была отсоединена от ViewModel).
  • Необходимо продвинутое использование Databinding Library, что требует времени на освоение.
  • Код в xml далеко не всем нравится.

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


Использовать ли вам этот паттерн – решайте сами. Но я вас предупредил.


PS: Databinding Library


Закончим, пожалуй, тем же, с чего и начали – Databinding Library. Мне она по-прежнему нравится. Но использовать её я собираюсь только в ограниченном количестве:


  • Чтобы не писать findViewById и setOnClickListener.
  • И чтобы создавать удобные xml-атрибуты при помощи BindingAdapter-ов (например, bind:font=”Roboto.ttf”).

И всё. Это даст плюсы, но не станет манить в сторону MVVM.


Если вы тоже планируете работать с Databinding Library, то вот вам немного полезной информации:


  • Вызывайте binding.executePendingBindings() в onViewCreated() после задания переменных биндингу. Это поможет, если вы хотите менять что-то в только что созданных view из кода. Не придётся писать view.post(), узнав, что view ещё не готова.
  • В тег <fragment> переменную передать (как можно в <include>) нельзя: https://code.google.com/p/android/issues/detail?id=175338.
  • Лямбды в xml в Databinding Library с особенностями. Нельзя писать без скобок (() -> method()). Нельзя блок кода. Зато можно опустить параметры, если не используются в методе (android:onClick=”@{() -> handler.buttonClicked()}”).
  • backtick (`) можно юзать вместо двойных кавычек (“).
  • В BindingAdapter-aх пишите только атрибуты (@BindingAdapter(“attributeName”)), namespace всё равно игнорируется. И в xml не важно, какой будет namespace. Но часто используют bind, чтобы отличать (bind:attributeName=”...”).
  • Сгенерированные databinding-классы искать тут: app/build/intermediates/classes/debug
  • Готовые адаптеры можно посмотреть тут.
  • Что почитать кроме документации:
    https://realm.io/news/data-binding-android-boyar-mount/
    https://www.bignerdranch.com/blog/descent-into-databinding/
    https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/
Поделиться с друзьями
-->

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


  1. Bringoff
    13.10.2016 13:30

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


    1. shifttstas
      01.02.2017 17:49
      +1

      Лампочки у меня светодиоды, нагрев слабый, между стеклом и потолком есть промежуток где свободно циркулирует воздух, полный комплект ~4000


  1. ivan_tatarchuk
    13.10.2016 14:55
    +8

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

    Я думаю, это касается не паттерна MVVM как такового, а скорее ограничений платформы на которой он применяется.


    1. terrakok
      13.10.2016 15:17

      В статье об этом и речь, что на android'е MVVM не позволяет сделать все в пределах паттерна


      1. areht
        13.10.2016 19:25
        +3

        Мне кажется в статье речь о том, что на андроиде люди MVP пытаются называть MVVM и страдают от результата.

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

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


        1. Jeevuz
          13.10.2016 19:39

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


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


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


          1. areht
            13.10.2016 21:10
            -1

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

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

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


            1. Jeevuz
              13.10.2016 21:56

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


              Давайте так:


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

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


              1. areht
                13.10.2016 23:18
                -1

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

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


                1. terrakok
                  13.10.2016 23:51

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


                  1. areht
                    14.10.2016 01:14
                    -1

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

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


                  1. mayorovp
                    14.10.2016 08:26
                    +1

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


                    1. Jeevuz
                      14.10.2016 08:53

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


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


                1. Jeevuz
                  14.10.2016 08:44

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


                  1. areht
                    14.10.2016 09:18
                    -1

                    Описание MVVM можно взять, например, там, где вы картинку от него взяли — https://msdn.microsoft.com/ru-ru/library/hh848246.aspx


  1. Sterk
    13.10.2016 15:05
    +6

    Я использую 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.

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


    1. Veikedo
      13.10.2016 16:05

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

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


      1. terrakok
        13.10.2016 16:14

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


        1. Veikedo
          13.10.2016 16:30
          +1

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


          Если вы про


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

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


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


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


          1. terrakok
            13.10.2016 16:41

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


            1. Veikedo
              13.10.2016 16:54

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


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


              1. Veikedo
                13.10.2016 17:04

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


                1. terrakok
                  13.10.2016 17:13

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


            1. areht
              13.10.2016 17:43

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

              Нормальный кейс — это последовательность открытых окон. Ну стек, в смысле, где "<" делает pop предыдущего. Не надо стек на флагах делать.


              1. terrakok
                13.10.2016 17:56

                Я для примера привел диалог. Кто говорит о стеке?
                Это может быть что угодно, Snackbar, например, или анимация какая-нибудь.
                Как сохранить такие вещи статическим набором данных?


                1. areht
                  13.10.2016 18:29

                  > Я для примера привел диалог. Кто говорит о стеке?

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

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

                  Вопрос «как сериализовать дерево объектов» риторический, я надеюсь?


                  1. senneco
                    13.10.2016 18:52

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


                    1. areht
                      13.10.2016 19:17
                      -1

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

                      Сам я больше по WPF


                      1. senneco
                        13.10.2016 19:30

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


                  1. terrakok
                    13.10.2016 19:48
                    +1

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


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


                    1. mayorovp
                      14.10.2016 08:29

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


                      1. Jeevuz
                        14.10.2016 08:57

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


                    1. house2008
                      17.10.2016 10:41

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

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

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


                      1. dmdev
                        17.10.2016 12:23

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

                        2) Нужно восстанавливать состояние вручную и проверять, что вьюха привязывается после восстановления. Запоминать команды с анимацией — плохая идея. Можно передавать команду скролла с анимацией, а в стейте сохранять без нее, только позицию.


        1. Sterk
          13.10.2016 17:27
          +1

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


          1. Jeevuz
            13.10.2016 19:31

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


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


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

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


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


            1. mayorovp
              14.10.2016 08:34

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


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


              1. Jeevuz
                14.10.2016 09:09

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


                1. mayorovp
                  14.10.2016 09:22

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


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


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


                  1. Jeevuz
                    14.10.2016 09:40

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


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


                    1. mayorovp
                      14.10.2016 09:54

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


    1. AIexandr
      13.10.2016 16:05

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

      Когда два года назад я занялся разработкой под Android, первое, что я сделал — начал искать подходящий фреймворк, реализующий MVVM. Выбрал RoboBinding, провозился с ним несколько дней, натыкаясь на проблемы и костыли. В итоге вернулся к старому доброму findViewById, использую такой подход до сих пор и ни на что не жалуюсь:)


      1. Seekeer
        13.10.2016 17:39

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


  1. Evgenij_Popovich
    14.10.2016 08:36
    +1

    Спасибо за статью, тема 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 и в несколько строчек делать всевозможные связи между полями (когда изменение одного поля, вызывает цепочку изменений других полей).


    1. Evgenij_Popovich
      14.10.2016 09:22

      Из жизненных примеров выгоды подключения RxJava к ObservableField

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

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

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

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


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



    1. Jeevuz
      14.10.2016 09:26

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


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


  1. buldo
    14.10.2016 09:10

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


    1. Jeevuz
      14.10.2016 09:13

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


      1. mayorovp
        14.10.2016 09:32

        Почему — "кроме"? Эти запросы через автобиндинг и проходят: https://habrahabr.ru/post/152003/


      1. buldo
        14.10.2016 11:30
        +1

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


  1. Evgenij_Popovich
    14.10.2016 09:22

    del мимо


    1. mayorovp
      14.10.2016 09:47
      +1

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


      int loadingCount;
      
      @Bindable 
      bool getLoading() {
        return loadingCount > 0;
      }
      
      AutoCloseable beginLoad() {
        loadingCount++;
        notifyPropertyChanged(BR.loading);
        return () -> {
          loadingCount--;
          notifyPropertyChanged(BR.loading);
        };
      }

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


      1. Evgenij_Popovich
        14.10.2016 09:53

        Да, это будет работать. Но что, если поля loading1, ..., loadingN находятся в других (разных) классах? Получается надо делать цепочку вызовов наверх, чтобы вызвать метод beginLoad


        1. mayorovp
          14.10.2016 09:58
          +1

          Я в таких случаях заводил LoadingViewModel и передавал ее в конструкторы других VM.


          1. Evgenij_Popovich
            14.10.2016 10:06

            Спасибо, тоже вариант. Rx ведь не единственно верный путь, просто помогает уменьшить количество boilerplate кода


  1. Yoto
    14.10.2016 21:32

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


    1. terrakok
      15.10.2016 12:54
      +1

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


      @Override
      public void onDestroy() {
        if (isRemoving() || getActivity().isFinishing() {
          //здесь очищаем Scope
        }
      }


    1. Jeevuz
      15.10.2016 14:29
      +1

      «логически» определяет, что данный Component будет жить столько же, сколько Scope.

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


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

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


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

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


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