- PVSM.RU - https://www.pvsm.ru -
На недавнем DroidCon Moscow 2016 [1] был доклад о MVVM c Databinding Library и доклад о библиотеке Moxy [2], помогающей работать с MVP. Дело в том, что за последние полгода мы успели опробовать оба подхода на живых проектах. И я хочу рассказать о своём пути от освоения Databinding Library и выпуска в продакшн проекта на MVVM до осознания, почему я больше не хочу использовать этот паттерн.
Посвящается всем, кого зацепила Databinding Library и кто решил строить приложение на MVVM, – вы отважные люди!
Начав разбираться с Databinding Library [3], я был под впечатлением. Те, кто уже знаком с ней, меня поймут, а для остальных, вот как выглядит работа с этой библиотекой:
Использование Databinding Library позволяет:
findViewById
и setOnClickListener
. То есть, указав id в xml, можно обращаться к view через binding.viewId
. И можно устанавливать вызовы методов прямо из xml;binding.setUser(user)
, а в xml указываем, к примеру, android:text = “@{user.name}”
;bind:url=”@{user.avatarUrl}”
.@BindingAdapter("bind:imageUrl")
public static void loadImage(ImageView view, String url) {
Picasso.with(view.getContext()).load(url).into(view);
}
Последний пункт особенно приятен для меня потому, что состояния всегда были сложной темой. Если на экране нужно отобразить три состояния (загрузка, данные, ошибка), это ещё ладно. Но, когда появляются различные требования к состоянию элементов в зависимости от данных (например, отображать текст только если он не пустой, или менять цвет в зависимости от значения), может понадобиться либо большой switch cо всеми возможными вариантами состояний интерфейса, либо много флагов и кода в методах установки значений элементам.
Поэтому то, что Databinding Library позволяет упростить работу с состояниями, – огромный плюс. К примеру, написав в xml android:visibility=”@{user.name != null ? View.VISIBLE : View.GONE}”
, мы можем больше не думать о том, когда надо скрыть или показать TextView с именем пользователя. Мы просто задаём имя, а видимость изменится автоматически.
Но, начав использовать 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. Давайте кратко его рассмотрим.
В паттерне Model-View-ViewModel [4] три основных компонента:
Связь и взаимодействие между собой этих компонентов мы видим на картинке:
Стрелками показаны зависимости: View знает о ViewModel, а ViewModel знает о Model, но модель ничего не знает о ViewModel, которая ничего не знает о View.
Процесс такой: ViewModel запрашивает данные у Model и обновляет её когда необходимо. Model уведомляет ViewModel, что данные есть. ViewModel берёт данные, преобразует их и уведомляет View, что данные для UI готовы. Связь между ViewModel и View осуществляется путём автоматического связывания данных и отображения. В нашем случае это достигается через использование Databinding Library. При помощи databinding’а View обновляется, используя данные из ViewModel.
Наличие автоматического связывания (databinding) является главным отличием этого паттерна от паттерна PresentationModel [5] и MVP [6] (в MVP Presenter изменяет View путём вызова на ней методов через предоставленный интерфейс).
Так я начал использовать 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 [7] и пользовательских scopes [8]. Не стану вдаваться в подробности, уже написано много хороших статей про dagger. Своими словами, происходит следующее:
Зачем фрагмент передаёт себя во ViewModel, используя интерфейс MvvmView? Это нужно для того, чтобы мы могли вызывать команды «вручную» на View. Не всё можно сделать при помощи Databinding Library.
При необходимости сохранения состояния в случае, когда система убила приложение, мы можем сохранять и восстанавливать состояние ViewModel, используя savedInstanceState фрагмента.
Примерно так всё работает.
Внимательный читатель спросит: «A чего мучаться с dagger custom scopes, если можно просто использовать Fragment как контейнер и вызвать в нём setRetainInstance(true)
». Да, так сделать можно. Но, рисуя схему, я учитывал, что в качестве View можно использовать Activity или ViewGroup.
Недавно я нашел хороший пример реализации MVVM [9], полностью отражающий нарисованную мной структуру. За исключением пары нюансов, всё сделано очень хорошо. Посмотрите, если интересно.
Нарисовав схему и обдумав всё, я понял, что именно меня не устраивало во время работы с этим подходом. Взгляните на схему снова. Видите толстые стрелки «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). Лично мне это не по душе.
Теперь расскажу о ещё одной проблеме. Представим ситуацию с поворотом телефона.
Получается, что нужно как-то хранить не только состояние View, представленное набором полей ViewModel, но также и методы, которые ViewModel вызывает на View.
Эту проблему можно решить, заводя во ViewModel поля-флаги на каждый отдельный такой случай. Это не очень-то красиво и не универсально. Но работать будет.
Проблема состояний натолкнула меня на мысли, что состояние объекта можно воссоздать двумя путями: набором параметров, характеризующим состояние, или набором действий, которые необходимо совершить, чтобы привести объект в нужное состояние.
Представьте себе кубик Рубика. Его состояние можно описать 9 цветами на одной из граней. А можно набором движений, которые приведут его из начального состояния в требуемое.
Может понадобиться всего один поворот, а может и намного больше девяти. Получается, в зависимости от ситуации, какой-то способ описания состояния лучше или хуже (меньше данных нужно).
Обдумывая способы воссоздания состояния, я не мог не вспомнить о библиотеке Moxy. Мои коллеги параллельно делали проект, используя паттерн MVP и эту библиотеку. Подробно я о ней рассказывать не стану, уже есть отличная статья от авторов [10].
В контексте моих рассуждений интересна одна особенность Moxy – она хранит состояние view как набор команд, вызванных на этой view. И когда я узнал об этом впервые, мне это показалось странным.
Но теперь, после всех размышлений (которыми я поделился с вами выше), я думаю, что это очень удачное решение.
Потому что:
Кроме того, этот подход даёт ещё один плюс. Он также, как и Databinding Library, по-своему решает проблему большого количества разных состояний. Тоже не придется писать огромный switch, изменяющий UI в зависимости от набора полей или названия одного из состояний, так как изменения воссоздаются набором вызовов методов.
И всё же я не могу совсем ничего больше не сказать про Moxy. По моему мнению и мнению моих коллег, на сегодняшний день она является лучшей библиотекой, которая помогает наладить работу с паттерном MVP. Она использует генерацию кода, чтобы минимизировать трудозатраты разработчика. Вы можете не думать о реализации паттерна, а думать о функционале своего проекта. А это хорошо.
Но хватит про MVP. Всё-таки речь у нас про MVVM, и пора подвести итоги.
Мне нравится MVVM как паттерн, и я не оспариваю его плюсы. Но в большинстве своём они те же самые, что у других паттернов, либо являются делом вкуса разработчика. Да и основной плюс даёт всё же databinding, а не сам паттерн.
Ведомый симпатией к MVVM, я реализовал проект на нём. Долго изучал тему, обдумывал, обсуждал и вынес для себя набор минусов этого паттерна:
Да, с этими минусами можно свыкнуться. Но после долгих раздумий я пришел к выводу, что не хочу работать с паттерном, который создает раздробленность подходов. И решил, что следующий проект буду писать, используя MVP и Moxy.
Использовать ли вам этот паттерн – решайте сами. Но я вас предупредил.
Закончим, пожалуй, тем же, с чего и начали – Databinding Library. Мне она по-прежнему нравится. Но использовать её я собираюсь только в ограниченном количестве:
findViewById
и setOnClickListener
.bind:font=”Roboto.ttf”
).И всё. Это даст плюсы, но не станет манить в сторону MVVM.
Если вы тоже планируете работать с Databinding Library, то вот вам немного полезной информации:
binding.executePendingBindings()
в onViewCreated()
после задания переменных биндингу. Это поможет, если вы хотите менять что-то в только что созданных view из кода. Не придётся писать view.post()
, узнав, что view ещё не готова.() -> method()
). Нельзя блок кода. Зато можно опустить параметры, если не используются в методе (android:onClick=”@{() -> handler.buttonClicked()}”
).@BindingAdapter(“attributeName”)
), namespace всё равно игнорируется. И в xml не важно, какой будет namespace. Но часто используют bind, чтобы отличать (bind:attributeName=”...”
).Автор: MobileUp
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/android/198803
Ссылки в тексте:
[1] DroidCon Moscow 2016: http://droidcon.moscow/ru/#program
[2] Moxy: https://github.com/Arello-Mobile/Moxy
[3] Databinding Library: https://developer.android.com/topic/libraries/data-binding/index.html
[4] Model-View-ViewModel: https://msdn.microsoft.com/en-us/library/hh848246.aspx
[5] PresentationModel: http://martinfowler.com/eaaDev/PresentationModel.html
[6] MVP: https://ru.wikipedia.org/wiki/Model-View-Presenter
[7] dagger: https://google.github.io/dagger/
[8] scopes: http://frogermcs.github.io/dependency-injection-with-dagger-2-custom-scopes/
[9] хороший пример реализации MVVM: https://github.com/patloew/countries
[10] отличная статья от авторов: https://habrahabr.ru/post/276189/
[11] https://code.google.com/p/android/issues/detail?id=175338: https://code.google.com/p/android/issues/detail?id=175338
[12] тут: http://androidxref.com/7.0.0_r1/xref/frameworks/data-binding/extensions/baseAdapters/src/main/java/android/databinding/adapters/
[13] https://realm.io/news/data-binding-android-boyar-mount/: https://realm.io/news/data-binding-android-boyar-mount/
[14] https://www.bignerdranch.com/blog/descent-into-databinding/: https://www.bignerdranch.com/blog/descent-into-databinding/
[15] https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/: https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/
[16] Источник: https://habrahabr.ru/post/312548/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best
Нажмите здесь для печати.