Annual Open Tech Conference - ISsoft Insights 2021. June 19. Learn more.
×Закрыть

Логіка відображення стану View в Android: проектуємо і тестуємо

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

«Што такоє? Што, спокойно ходіть нє можетє?» Лесь Подерв’янський, «До$*я масла»

У цьому матеріалі зібрані мої думки щодо того, як краще облаштувати стани View в Android. Так, одна View має кілька станів. Так вже склалося — View одна, а відображається по-різному. Наприклад, ми отримуємо дані з мережі (хто цього не робив, нехай першим кине в мене дизлайк) і бачимо такий ланцюжок станів:

СтанСтан UI
тільки включились та нумо ініціалізувати все, що цвяхами не прибитоIniState
Може щось напишемо, може покажемо літаючого ковбасного монстра (infinite ProgressBar )
припадаємо до джерела (DataSource)відображається лоадер LoadDataState
відображається помилка LoadErrorState
відображення списку данихвідображається список ListShowState

відображення екрану порожнього списку
ListEmptyState

Приклад логіки фрагменту з програми (тест слуху, робота зі списком пройдених тестів)

Для такого простого варіанту використання у нас вже цілих чотири стани та нульовий (init).

А зараз трохи страшилок правди життя.

Лишень уявіть, що сюди додається нова фіча. Наприклад: раптом знадобилося отримати список відсортованих/відфільтрованих результатів на тому ж самому екрані. Або, якщо ваш застосунок був у режимі без зареєстрованого користувача, то йому потрібно/не потрібно показувати маркетинговий банер, а тепер цей користувач здійснив вхід у застосунок, і вам потрібно показати йому іншу інформацію. А на додачу ще й необхідно отримати з локальної бази ще трохи даних, перевірити відповідність із даними зареєстрованого користувача, при цьому показувати стан перевірки. А як щодо завантаження з мережі ще однієї порції даних, за необхідності розбиваючи її на сторінки? З часом ці стани множаться відповідно до зростання та вдосконалення сценаріїв та вимог.

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

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

Наскільки незручно працювати з непокритим тестами кодом можна подивитися, виконавши чекаут на коміт.

Перехід по станам там відбувається з імовірністю 50%. Спробуйте знайти помилку в коді, перевіряючи результат виконання — відчуйте себе в казино.

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

Отже, вашій увазі пропонується: концепція побудови єдиного стану для View — рівня показу, яка об’єднує у собі дані та методи, необхідні для вибору способу візуалізації призначеного для користувача інтерфейса. Я продемонструю, як можна абстрагуватися від логіки стану відображення даних за допомогою інтерфейсів, а також протестувати її.

І відразу disclaimer: це не ідеальне рішення для кожної програми і кожного View. Свою думку з цього питання запрошую висловити в коментарях.

Поки ми не зайшли далеко, домовимось про умови, яким має відповідати рішення (сценарії):

  1. Об’єкт стану повинен мати однозначне зіставлення зі станом призначеного для користувача інтерфейсу.
  2. Ланцюжок викликів для створеного стану не повинен змінюватися з часом.
  3. Додавання нових станів не повинно впливати на існуючі стани.
  4. При додаванні нового об’єкта стану повинен бути передбачений захист від випадкового не зіставлення зі станом призначеного для користувача інтерфейсу.
  5. Логіка вибору стану має буть покрита тестами.

Цей концептуальний перформанс буде відбуватися з використанням Android Architecture Components — ViewModel і, відповідно, згідно паттерна MVVM.

Трохи теорії: Що таке MVVM паттерн?

MVVM — це архітектурний паттерн, розшифровується як Model-View-ViewModel. Народився від MVP, представлений в 2005.

Щоправда, якщо розташувати букви в порядку їхньої взаємодії, то вийде View-ViewModel-Model, тому що в реальності ViewModel знаходиться саме посередині, поєднуючи View та Model.

Для зацікавлених приведу історичну довідку про розвиток легалайзу появу три- і чотири-літерних «заклинань». А також дам опис їх відмінностей:

  • MVC: описаний у 1979 році. Доступ до даних (Model) є і у View, і у контролерів. Тобто View може перебудовуватись самостійно лише на підставі зміни даних в Model або взаємодії користувача з View (натискання на екран), або сама змінити дані в Model і тільки повідомити контролеру про це. Controller може змінювати стан View на підставі своїх джерел даних (например, зовнішні запити, натискання на кнопку) і також може змінювати Model.
  • MVP: описаний у 1990 році. Presenter є посередником між View та Model.View і Model змінюються даними через встановлене API. Відображення у View залежить тільки від даних, які встановив Presenter.
  • MVVM: представлений у 2005році. ViewModel також є посередником між View та Model. Але ViewModel не може безпосередньо впливати на View, а лише є джерелом даних і має можливість через функції виклику передавати актуальні дані. Те, як відображати, визначається на рівні View.

Детальніше — в цій статті та в цій.

View — це абстракція для Activity, Fragment або будь-якою іншою навіть кастомною View (Android Custom View). View повинна максимально абстрагуватися від реалізації і даних, ми повинні уникати писати якусь логіку в неї. Також View не повинна знати нічого про інші частини програми. Вона повинна зберігати посилання на екземпляр ViewModel, і всі дані, які потрібні View, повинні приходити звідти.

ViewModel або Interactor. Відповідає за прийом дій користувача від View і віддачу оброблених і підготовлених даних для відображення у View.

Model — або в термінах clean — Use Case. Одна або кілька моделей отримують та/або обробляють специфічні операції з даними і логіку для інших фундаментальних подій системи та взаємодіють з ViewModel.

Підбиваючи підсумок

View відповідає за вигляд і відображення даних та взаємодію з користувачем.

ViewModel — за обробку взаємодії з користувачем, містять дані і логіку про те, коли ці дані повинні бути отримані і коли показані.

Model — містить логіку обробки специфічних операцій з даними і логіку для інших фундаментальних подій системи.

Трохи теорії: стан і ДКА (FSM)

Ми почули на початку про стани View. І чогось кудись переходить. Хм, все-таки, універ — це сила, а її мало не буває. Згадую, на парах я щось таке чув. Так це ж про кінцевий автомат або коротко — про КА (SM або StateMachine)!

Що таке кінцевий автомат? Все — від простих поведінкових шаблонів до розподілених систем — містить їх.

Давайте розберемо докладніше. У коді регулярно зустрічаються комутативні функції. Це така примітивна абстракція без власної пам’яті: на вхід аргумент, на виході якесь значення. Вихідне значення залежить тільки від вхідного. Приклад: switch або if-else.

Але доволі часто в реальному світі додаток може перебувати в одному з декількох станів, які змінюють один одного. І на однакові впливи реагувати по-різному. Тобто нам необхідно, щоб наступне значення функції залежало від попереднього. А іноді потрібно враховувати кілька попередніх.

Тут вже приходимо до якоїсь абстракції з власною пам’яттю. Це і називається автомат. Значення на виході автомата залежать від значення на вході і поточного стану автомата. Якщо у цього автомата кількість значень на виході кінечна — то це кінцевий автомат (SM). Ось і перша абревіатура з 2х літер. Але ДКА (FSM) чомусь трилітерні?

Найпростіший SM, в якому може бути один стан в поточний момент часу, має детермінованість. Детермінованість означає, що для всіх станів є максимум і мінімум одне правило для будь-якого можливого вхідного символу, тобто, наприклад, для «стану1» не може бути двох переходів з однією і тією ж вхідною послідовністю.

Для повноти опису згадаємо про існування недетермінованих кінцевих автоматів (НКА або ж NFA, Nondeterministic Finite Automaton). Висловлюючись простіше, в NFA насипано синтаксичного цукру у вигляді вільних переходів, недетермінованости і безлічі станів. NFA може бути представлений певною структурою з FSM. Прикладом є побудова з FSM еквівалентного NFA за алгоритмом Томпсона.

Переваги FSM:

  • Дозволяє відокремити автомат і стан системи від коду, яким він керує. Або іншими словами — розділити реалізацію стану системи від реалізації керуючих функцій системи.
  • Немає фатального недоліку.
  • Можливість збереження стану.
  • Прозорі і детерміновані дії в станах і переходах між ними, їх легко змінити і приблизно ясно де вони знаходяться.

Як будуємо FSM

Беремо велику складну хмару станів і як салямі розрізаємо на скибочки — на маленькі дискретні стани і розмічаємо граф зв’язків (переходів) між ними.

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

Трохи теорії: Доповнюємо MVVM

Стани

Стани прописані в класі MainFragmentUiStatesModel. Він оголошений у вигляді sealed class. Так зроблено тому, що кожна з реалізацій sealed class сама по собі є повноцінним класом. Це означає, що кожен з них може мати свої власні набори властивостей незалежно один від одного. Цим синтаксичним цукром ми посипаємо умову 4 з наших сценаріїв: «При додаванні нового об’єкта стану повинен бути передбачений захист від випадкового не зіставлення зі станом призначеного для користувача інтерфейсу».

Граф переходів

Тримати в ViewModel не тільки стан, а й перелік станів і логіку переходу зі стану в стан якось не дуже зручно. Їх непогано б винести окремо, щоб уникнути дублювання і дотримати принцип єдиної відповідальності.

Для цього у View має бути якийсь-то контракт, згідно з яким View буде відображати дані з ViewModel. відповідно, ViewModel буде керувати відображенням даних просто змінюючи стан View. Отже, додаємо Contract до View.

Repository

В Android-співтоваристві поширене визначення Repository як об’єкта, який надає доступ до даних.

Ілюстрацію взято звідси

У View з’являється контракт, який відповідає за стани і логіку їх відображення. Методи для перемикання View в різні стани визначено в контракті і описані в View, яка реалізує MainFragmentViewStatesRenderContract. Самі стани знаходяться в MainFragmentUiStatesModel.

ViewModel — це абстрактне ім’я для класу, що містить дані і логіку їх підготовки до відображення; логіку, коли ці дані повинні бути отримані і як будуть показані. Також ViewModel зберігає поточний стан. У прикладі це mViewState зі значеннями типу класу MainFragmentUiStatesModel. Коли потрібно змінити стан View, ViewModel змінює mViewState. Потім View, яка отримує повідомлення про цю зміну стану, використовує контракт, щоб визначити, як саме змінюватись.

Також ViewModel зберігає посилання на одну або кілька DataModel. У нашому випадку це ExampleRepository. Все дані ViewModel отримує від них.

Завдяки цьому ViewModel не знає, наприклад, звідки отримує дані Repository — з бази даних або з сервера. Крім того, ViewModel не повинна знати про View.

А тепер усе разом

ViewModel генерує події. View, згідно контракту, відображає стани. Дані беруться з репозиторію (Repository).

MVVM, якщо реалізований правильно, є чудовим способом розбити код на менші частини (так, «салямі принцип» працює і тут) і зробити його більш «передбачуваним». Це допомагає нам слідувати принципам SOLID, тому наш код легше підтримувати.

У такій реалізації ми виконуємо умови з 1 по 3 наших сценаріїв:

  1. Об’єкт стану повинен мати однозначне зіставлення зі станом призначеного для користувача інтерфейсу.
  2. Ланцюжок викликів для створеного стану не повинна змінюватися з часом.
  3. Додавання нових станів не повинно впливати на існуючі стану.

Почнемо. Написання тестів

Shu Ha Ri, три бажання ... TDD також не минула ця доля — тут теж присутня магія трьох:

  1. Написання тесту, що дає збій, для невеликого фрагменту функціоналу.
  2. Реалізація функціоналу, яка призводить до успішного проходження тесту.
  3. Рефакторинг старого і нового коду для того, щоб підтримувати його в добре структурованому і читабельному стані.

TDD ще називають циклом «червоний, зелений, рефакторинг» ( «Red, Green, Refactoring»).

Підготуємось

Почнемо з написання тестів на контракт View.

Нагадую: клас, де знаходяться View стани, — це MainFragmentUiStatesModel. Відповідно контракт MainFragmentViewStatesRenderContract by default містить:

  • метод render (viewState: MainFragmentUiStatesModel);

і по одному методу відображення до відповідного стану:

  • showIni () до IniState
  • showLoadCounterPercentData до LoadCounterPercentDataState
  • і т. ін.

Господині на замітку: в Android Studio є комбінація клавіш Ctrl + Shift + T (⇧⌘T) у якій ховається меню автогенерації тесту.

Використовуємо заклинання автогенерації тесту на MainFragment і вибираємо для тесту всі методи з MainFragmentViewStatesRenderContract.

Стан коду, після автогенерації тесту, див. комміт

Налаштування тестового класу

Вказуємо перед найменуванням класу, що будемо використовувати Mockito:

@RunWith(MockitoJUnitRunner::class)

Ми перевіряємо contract і нам потрібно створити його як змінну, що реалізує інтерфейс MainFragmentViewStatesRenderContract. Оскільки Mockito не вміє перевіряти виклики методів через інтерфейс, нам доведеться реалізувати його в порожньому класі. Назвемо його MockForTestMainFragmentViewStatesRenderContract, оголосивши як open class і подбаємо про те, щоб анотувати його за допомогою @Spy.

Червоний. Перший тест: функція testRenderInitState ()

Господині на замітку: Хорошим тоном вважається розбивати тести на три частини: Налаштування (Setup), Дія (Act) і Перевірка (Assert).

Налаштування (Setup):

Ця частина порожня, так як особливо нема чого налаштовувати.

Дія(Act):

Зазвичай це виклик функції, яку ми тестуємо. В цьому випадку ми перевіряємо, чи може функція render () відображати стан IniState.

Перевірка(Assert):

Перевіряємо, що правильна функція у contract викликається після виклику render (), а інші відповідно, не викликаються:

// Assert
verify(contract).showIni()
verify(contract, never()).showLoadCounterPercentData(any())
verify(contract, never()).showLoadError(any())
verify(contract, never()).showListEmpty()
verify(contract, never()).showListShow(any())

Запускаємо тест клас на перевірку:

Результат виконання:

Чого і слід було очікувати. Стан коду на цей момент: див. комміт

Зелений

Давайте додамо в MainFragmentViewStatesRenderContract мінімум коду, тільки щоб тест пройшов:

fun render(viewState:MainFragmentUiStatesModel){
  showIni
()}

Досить одного рядка :)

Стан коду на цей момент: див. комміт.

Наступна ітерація

Червоний

Так як процес написання тестів буде однаковий для решти станів, щоб скоротити «простирадло», відразу пишемо тести на решту станів:

@Test
fun testRenderLoadCounterPercentData() {
  // Act
  contract.render(MainFragmentUiStatesModel.LoadCounterPercentDataState(50))
  // Assert
  verify(contract, never()).showIni()
  verify(contract).showLoadCounterPercentData(50)
  verify(contract, never()).showLoadError(any())
  verify(contract, never()).showListEmpty()
  verify(contract, never()).showListShow(any
())}

@Test
fun testRenderLoadError() {
  // Act
  contract.render(MainFragmentUiStatesModel.LoadErrorState("Error"))
  // Assert
  verify(contract, never()).showIni()
  verify(contract, never()).showLoadCounterPercentData(any())
  verify(contract).showLoadError("Error")
  verify(contract, never()).showListEmpty()
  verify(contract, never()).showListShow(any
())}

@Test
fun testRenderListEmpty() {
  // Act
  contract.render(MainFragmentUiStatesModel.ListEmptyState)
  // Assert
  verify(contract, never()).showIni()
  verify(contract, never()).showLoadCounterPercentData(any())
  verify(contract, never()).showLoadError(any())
  verify(contract).showListEmpty()
  verify(contract, never()).showListShow(any
())}

@Test
fun testRenderListShow() {
  // Act
  contract.render(MainFragmentUiStatesModel.ListShowState(ArrayList()))
  // Assert
  verify(contract, never()).showIni()
  verify(contract, never()).showLoadCounterPercentData(any())
  verify(contract, never()).showLoadError(any())
  verify(contract, never()).showListEmpty()
  verify(contract).showListShow(any
())}

свистимо — тарган не біжить

Результат виконання:

Стан коду на цей момент: див. комміт

Зелений

Додамо в MainFragmentViewStatesRenderContract трохи коду — аби лише тест пройшов:

fun render(viewState:MainFragmentUiStatesModel){
  when (viewState){
    is MainFragmentUiStatesModel.IniState -> {
      showIni
    ()}
    is MainFragmentUiStatesModel.LoadCounterPercentDataState -> {
      showLoadCounterPercentData(viewState.
    percent)}
    is MainFragmentUiStatesModel.LoadErrorState -> {
      showLoadError(viewState.
    errorCode)}
    is MainFragmentUiStatesModel.ListEmptyState -> {
      showListEmpty
    
  
()}}}

Я пропустив покриття ListShowState стану, і компілятор мені нагадав про це:

Не можу не відзначити зручність синтаксичного цукру — MainFragmentUiStatesModel оголошений як sealed class, і тепер компілятор контролює повноту покриття всіх станів методами перемикання View в директиві типу when (viewState). Хоча ми і передбачили відсутність можливості дублювання коду для цього вибору і покрили його тестами, але все одно це зручна та корисна властивість.

Додаю покриття стану ListShowState:

is MainFragmentUiStatesModel.ListShowState -> {
  showListShow(viewState.
listItem)}

Запускаю тести:

Висновок

Тепер у нашому View немає коду, що відповідає за перемикання станів. Реалізація всіх методів відображення перемикання станів контролюється нашим контрактом. Код, що відповідає за перемикання, знаходиться там же. Його легко задіяти для підтримки подібних станів у інших View проекту.

Також ми виконуємо умову 5 з наших сценаріїв: «Логіка вибору стану має бути покрита тестами».

І ще один плюс використання TDD для написання коду — перегляд послідовності коммітів полегшує розуміння логіки побудови коду.

Код прикладів, розглянутих у статті знаходиться тут: Git репозиторій

Буду радий спілкуванню по темі (і не по темі теж :)) в коментарях.

Графоманські Творчі плани

У наступній частині статті ми візьмемось за реалізацію ViewModel і Repository. У ній ми будемо використовувати Flows, елементи Functional Programming (за посиланням, до речі, цікава стаття про питання функціонального програмування на Kotlin) з використанням бібліотеки від Arrow. ФП використаємо в гомеопатичних кількостях, а саме — тип Either для роботи з джерелами даних і обробки помилок в функціональному стилі. І, звичайно, підемо «дорогою з хмарками» з TDD.

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

Написати автору, що «функціональщина» це не «дорога з хмарками», а Lucy In The Sky With Diamonds, завжди можна у коментарях.

👍НравитсяПонравилось1
В избранноеВ избранном0
Подписаться на тему «Android»
LinkedIn

Похожие статьи

Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Допустимые теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Натягивал когда-то FSM (а если точнее, паттерн State) еще на MVP, полет нормальный был. Презентер Выполнял роль Context’а, который был проксей между state и view, каждый state, собственно, был отдельным классом, и мог либо по своему реагировать на пользовательские события, на которые подписывался через контекст, либо переключаться на следующий стейт.

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

SomeFragment.kt:
..
presenter.onEvent(Event.ButtonClicked)
..

SomePresenter.kt:
..
currentState.onEvent(Event.ButtonClicked)
..

YetAnotherState.kt:
..
when(event){
..
is Event.ButtonClicked -> {
    if (someComplexCondition){
        context.fetchSomeData()
        context.showLoading(true)
        context.setState(Loading())
    } else {
        context.setState(SomeRequrementsDialogState(missingRequrements))
    }
}
..

Преимущества, собственно, такого подхода в сравнении с тем, что предлагает renderer в том, что когда появляется сетка состояний, и их больше трех, то там выходит when в when в when. Отдельные класы это «флетят», размазывая логику, и тут уже надо только переходы между стейтами нормально проработать. А стейты выглядят примерно так: AuthorizedWithLocationSearch, AuthorizedWithoutLocationSearch, UnathorizedWithLoactionSearch, UnathorizedWithoutLoactionSearch, AuthorizedWithLocationButInAnotherCitySearch и т.д. Любое новое требование добавляется/тестируется дотстаточно легко, надо найти состояния, с которых мы можев выйти на новое, и проработать куда мы можем выйти из этого состояния, а всю остальную логику можно «выгрузить» из оперативной памяти.

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

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

«Африкан Свиридович. Та не кажи, ці коти вже з@#али, я їх усіх віддав би на мило!
Баба Химка. Він, клятий, мишей зовсім не чіпа. Каже, шо з мишами треба
поводитись так, як хочеш, шоб миші поводились з тобою...
Саломон Самсонович. Ти диви, Толстого читав!
Свирид Опанасович. Топити треба.
Голос з сіней. Не треба.
Входять мишки — Вєра і Надєжда. При першому ж погляді на их становиться ясно,
чому Мурзік увльокся Толстим.»
Лесь Подерв’янський.Казка про рєпку

Що таке MVI паттерн?

Як написав автор, MVI це спроба переосмислення і повернення до витоків, до істинного MVC. cycle.js.org/...​-what-mvc-is-really-about

MVI — представлений в 2015 році. Додано та змінено в порівнянні з MVVM:

Intent — функція, яка приймає вхідні дані від користувача (наприклад, події призначеного для користувача інтерфейсу, такі як події click) і переводить в те, що буде передано як параметр функції model (). Це може бути проста рядок для установки значення моделі або більш складна структура даних, наприклад, об’єкт.

Model — Функція, яка використовує вихідні дані з функції intent () в якості вхідних даних для роботи з моделлю. Результат роботи цієї функції — нова модель (зі зміненим станом). При цьому потрібно, щоб дані були незмінними. По суті, Модель здійснює виклик бізнес-логіки додатка (будь-то Interactor, UseCase, Repository) і в результаті повертає новий об’єкт моделі.

View — Функція, яка отримує на вході модель і просто відображає її. Зазвичай ця функція виглядає як view.render (model).

Детальніше про групу архітектур об’єднану ідеєю «односпрямованого потоку даних» см. Тут докладно і з картинками, див. Тут

Основна проблема, яку це диво вирішує— що в MVP і MVVM станом управляє Presenter або ViewModel. До чого це призводить:

1. У бізнес-логіки є власний стан, так само, як у Presenter або ViewModel. Ви намагаєтеся синхронізувати стан бізнес-логіки й Presenter, щоб вони були однаковими. Встановлюєте видимість якогось віджета прямо з View, або Android сам відновлює стан з bundle під час перевтілення.

2. Presenter і ViewModel мають довільну кількість вхідних точок. View запускає подію, яка обробляє Presenter. Але і Presenter має багато каналів виведення — як view.showLoading () або view.showError () в MVP. А ViewModel пропонує множинні Observables. Це призводить до конфліктуючих станів View, Presenter і бізнес-логіки, особливо при роботі з декількома потоками.

Говорячи по іншому у нас одночасно може з’явитися відображення помилки та лоадер з процентом )

Для цього у View має бути якийсь-то контракт, згідно з яким View буде відображати дані з ViewModel.

По MVVM вьюха должна просто подписаться на события из вьюмодел и отображать состояние именно вьюмодела, не имея никакого собственного состояния. В Андроид на сегодня есть только один нормальный способ это сделать. Databinding.
Он уже включает в себя всю логику контракта путём привязывания значений состояния вьюмодела к атрибутам вьюхи.
Вьюхе не нужно ничего знать ни о состояниях, ни о переходах между ними. Её задача — просто наблюдать за вьюмоделом и делать как он, не задумываясь.

вьюха может быть обыкновенным фрагментом и подписываться на стримы с ViewModel
хранишь ли ты состояние во фрагменте — это уже другой вопрос

хранишь ли ты состояние во фрагменте — это уже другой вопрос

Это не вопрос вообще. Фрагмент/Активити — не место для хранения состояния в принципе. Потому что у них свой жизненный цикл, не связанный в общем случае с состоянием приложения. Поэтому они только ретранслируют пользователю состояние вьюмодела и отвечают за навигацию, всё.

подписываться на стримы с ViewModel

Обработка этих стримов во фрагменте приведёт к огромному количеству бойлерплейта в нём. Который, кстати, никак нельзя будет протестировать. Поэтому — только через прямой байндинг стримов к форме.

. Databinding.
Он уже включает в себя всю логику контракта путём привязывания значений состояния вьюмодела к атрибутам вьюхи.

Тут можно ли пояснить, что имеется ввиду под «состояния вьюмодела». Так как, по моему, логика поведения != логика отображения. У вьюмодел этих самых состояний может быть гораздо больше чем у вью. Например добавочная логика состояний и переходов вьюмодел (fsm) по реакции на действия пользователя, ну и на состояние даных своя логика, и на глобальные события еще одна.

Тут можно ли пояснить, что имеется ввиду под «состояния вьюмодела».

Это комбинации значений полей вьюмодела, которые прибиты к XML форме вьюхи.

и на глобальные события еще одна.

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

Глобальные события — события системы. Например изменение состояния интернета. Ну а равноправность стейтов —это ближе к конкретным бизнес правилам. В общем случае это предсталяется в виде нескольких FSM, упростить к конкретным правилам всегда можно.

Это комбинации значений полей вьюмодела, которые прибиты к XML форме вьюхи.

А тогда набор настроек данных полей

которые прибиты к XML форме вьюхи.

можно ли назвать состоянием вью?

можно ли назвать состоянием вью?

Нет, потому что это состояние вьюмодела.
Если у вью есть собственное состояние, о котором вьюмодел ничего не знает, то у нас появляется view controller для управления этим состоянием и мы получаем MVC паттерн со всеми его прелестями, плюс невозможность протестировать юнит-тестами логику состояний вьюхи.

А тогда набор настроек данных полей

Этими настройками могут быть текст, картинка, цвет, видимость, размеры, шрифт, любая характеристика элементов графического интерфейса.
И поведение приложения — в том числе. Функции тоже очень хорошо прибиваются прямо к графическому интерфейсу.

Функции тоже очень хорошо прибиваются прямо к графическому интерфейсу.

Ну как раз это и проблема. Реактивность и много функций вызываемых асинхронно приводят иногда к интересным состояниям Вью

Реактивность и много функций вызываемых асинхронно приводят иногда к интересным состояниям Вью

Я нигде не спорю с утверждением, что MVI нужен и все состояния ViewModel нужно явно описывать.

так состояния ЮИ хранятся в вьюмодел и упраляются вьюмоделом. Прсто у него есть еще состояния кроме ЮИных

Прсто у него есть еще состояния кроме ЮИных

Если функция вью состоит в отображении пользователю графического интерфейса и вы написали там какой-то код, который с этим не связан, то сами себе усложнили жизнь и создали проблемы на будущее.
1) Вью не должен хранить никакого стейта. Это не его функционал.
2) Там не должно быть никакого кода, не связанного с отображением GUI. Это не его задача.

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

Золотые слова.
1 -точно
2 так и есть

И вот кстати из статьи:

Тепер у нашому View немає коду, що відповідає за перемикання станів. Реалізація всіх методів відображення перемикання станів контролюється нашим контрактом. Код, що відповідає за перемикання, знаходиться там же. Його легко задіяти для підтримки подібних станів у інших View проекту.

Это безусловно хорошее улучшение.
Осталось убрать из вью любой код, связанный с этими состояниями. Я честно говоря, не понимаю, почему он там.

Отлично. Теперь мне осталось понять, как об этом написать и просто и понятно. Бо из треда следует, что было непанятна

Так как, по моему, логика поведения != логика отображения.

При наличии higher order functions в Котлин это непринципиально. Ничто не мешает засеттить в качестве одного из полей состояния вызов функции.

бох ты мой человек MVVM открыл, уже б про что то современное писал

О, хоть хтось по суті статті. А про що саме сучасніше хтілось би вельмишановному?

хотя бы MVI или как нахлобучить bloc на андроид

Про цей MVI — те що в статті описане, як раз і реалізує його плюси, та оминає мінуси, якщо ми базуємось на визначеннях, аналогічних з blog.mindorks.com/...​inners-step-by-step-guide або proandroiddev.com/...​and-channels-d36820b2028d В статтю звичайно, додам, спасибі за зауваження, просто планувалось у спін-оффі разом з розповіддю про додаткові FSM на користувацькі дії та внутрішні стани.
Про BLoC добра вказівка, „If you know about the MVVM (Model-View-ViewModel) pattern, the BLoC is the replacement for ViewModel. The BLoC is responsible for managing the state for View, and the Model (or Repository) helps the BLoC to access data no matter local or remote. In Flutter, there is no data binding” це звідси medium.com/...​ there is no data binding.
Про BLoC та MVI також гарне зауваження, що треба розглянути оці базворди в одній системі вимірів.

Звичайно, то що вийшло після доповнення «канонічного» MVVM більш нагадує інше трибуквенне слово MVI (Model-View-Intent). Загалом то схоже, різниця в загальному то термінологічна, ідеологічно це те ж потрапляє в тренд «односпрямованого потоку даних». Детальніше ми це розглянемо у другій частині, коли будемо розглядати реалізацію ViewModel.

Как же я вздохнул с облегчением когда перешел на Flutter.

у флатера немного по другому юай организован, но в остальном все так же

ua.zt.mezon.graphomania — гарний пекедж, чо
mezon.zt.ua майже не спалився. А після прочитання статті просто погляньте в реалі, яку якісну поробку вони створили. Про https тихо промовчу.

То вже років з 9 як зомбі продж. А таки да, за «вони» спасибі, там я більшість часу як «челавек-аркестр» виступав на орені. А сам мезон закрив, як отримав повістку на мобілізацію ) Так що пекедж нейми то ностальгія. от згадалось жеж

Такого плану пекдж нейми в мене йдуть на гарнір до дописоприступів )

Ото як на таке питання відповісти? ностальгія, та як сувенір на згадку. Може, щось знову почну, хай живе. До речі глянув і аж здивувався, за 9 років не дефейснули, правда логіка накрилась, бо тоді я всю авторизацію зробив на соцсітках а з мейл та одноглазниками і яндексом якось зараз не дуже. Та й інтеграція з ними там тісна була, якраз у теперішніх трендах.

Хм. Спасибі за нагадування за забутий домен. На ньому й розкладу приклади для статті по флаттеру.

Model-View-ViewModel

Пропоную новий патерн MVVM?P!
Model-View-ViewModel-??????-PROFIT!

Подписаться на комментарии