Як перестати використовувати mvvm

Як перестати використовувати mvvm

Присвячується всім, кого зачепила Databinding Library і хто вирішив будувати додаток на MVVM, - ви відважні люди!

Databinding Library

Почавши розбиратися з Databinding Library. я був під враженням. Ті, хто вже знайомий з нею, мене зрозуміють, а для інших, ось як виглядає робота з цією бібліотекою:

Як перестати використовувати mvvm

Використання Databinding Library дозволяє:

  • Позбутися від викликів findViewById і setOnClickListener. Тобто, вказавши id в xml. можна звертатися до view через binding.viewId. І можна встановлювати виклики методів прямо з xml;
  • Зв'язати дані безпосередньо з елементами view. Ми викликаємо binding.setUser (user). а в xml вказуємо, наприклад, android: text = "@";
  • Створювати кастомниє атрибути. Наприклад, якщо ми хочемо завантажувати зображення в ImageView за допомогою бібліотеки Picasso, то можемо створити BindingAdapter для атрибута "imageUrl", а в xml писати bind: url = "@".
    Такий BindingAdapter буде виглядати так:
  • Зробити стан view залежним від даних. Наприклад, чи відображається індикатор завантаження, буде залежати від того, чи є дані.
  • Останній пункт особливо приємний для мене тому, що стану завжди були складною темою. Якщо на екрані потрібно відобразити три стану (завантаження, дані, помилка), це ще добре. Але, коли з'являються різні вимоги до стану елементів в залежності від даних (наприклад, відображати текст тільки якщо він не порожній, або міняти колір залежно від значення), може знадобитися або великий switch cо всіма можливими варіантами станів інтерфейсу, або багато прапорів і коду в методах установки значень елементам.
    Тому те, що Databinding Library дозволяє спростити роботу з станами, - величезний плюс. Наприклад, написавши в xml android: visibility = "@". ми можемо більше не думати про те, коли треба приховати або показати TextView з ім'ям користувача. Ми просто задаємо ім'я, а видимість зміниться автоматично.

    Але, почавши використовувати databinding активніше, ви отримаєте в xml все більше і більше коду. І, щоб не перетворювати layout в смітник, ми створимо клас, в який винесемо цей код. А в xml залишатимуться тільки виклики властивостей. Наведу маленький приклад. Припустимо, є клас User:

    А в UI ми хочемо бачити повне ім'я та пишемо в xml:

    Це не дуже хочеться бачити в xml. і ми створюємо клас, в який виносимо цю логіку:

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

    У прикладі клас успадковується від BaseObservable, a в коді викликає notifyPropertyChanged (), але це не єдиний спосіб. Можна також обернути поля в ObservableField, і залежні елементи UI будуть оновлюватися автоматично. Але я вважаю такий спосіб менш гнучким і рідко його використовую.

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

    Набагато краще, чи не так?

    Отже, у нас з'явився ViewModel клас, який виступає в ролі прошарку між даними і view. Він займається перетвореннями даних, керує тим, які поля (і пов'язані елементи UI) і коли оновлюються, містить логіку того, як одні поля залежать від інших. Це дозволяє очистити xml від коду. Крім того, зручно використовувати цей клас для обробки подій з view (натискання тощо).

    І тут до нас приходить думка: Якщо у нас вже є databinding. є ViewModel клас, що містить логіку відображення, то чому б не використати патерн MVVM?

    Ця думка приходить неминуче. Тому що те, що ми маємо в даний момент дуже і дуже близько до того, що з себе представляє патерн MVVM. Давайте коротко його розглянемо.

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

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

    Зв'язок і взаємодія між собою цих компонентів ми бачимо на зображенні:

    Як перестати використовувати mvvm

    Стрілками показані залежності: 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:

    Як перестати використовувати mvvm

    Розглянемо, що в підсумку виходить:

    ViewModel містить поля, які використовуються в xml для Біндінга даних (android: text = "@"), обробляє події викликані на View (android: onClick = "@"). Вона запитує дані у 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:

    Що, якщо нам потрібно встановити цей текст, використовуючи view.post (new Runnable ()). (Не думаємо навіщо, думаємо як)

    Можна зробити BindingAdapter, в якому створити атрибут «byPost», і зробити, щоб враховувалася наявність перерахованих атрибутів у елемента.

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

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

    Бачите, скільки всього потрібно зробити, щоб реалізувати дуже навіть просте дію?

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

    Ось тут і проявляється проблема подвійності: ми працюємо з View двома різними способами. Один - автоматичний (через стан даних), другий - ручний (через виклики команд на view). Особисто мені це не до душі.

    проблема станів

    Виходить, що потрібно якось зберігати не тільки стан View, представлене набором полів ViewModel, але також і методи, які ViewModel викликає на View.

    Цю проблему можна вирішити, заводячи у ViewModel поля-прапори на кожен окремий такий випадок. Це не дуже-то красиво і не універсально. Але працювати буде.

    про стану

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

    Уявіть собі кубик Рубика. Його стан можна описати 9 квітами на одній з граней. А можна набором рухів, які приведуть його з початкового стану в необхідний.

    Може знадобитися всього один поворот, а може і набагато більше дев'яти. Виходить, в залежності від ситуації, якийсь спосіб опису стану краще або гірше (менше даних потрібно).

    В контексті моїх міркувань цікава одна особливість 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, то ось вам трохи корисної інформації:

    Поділитися