Как стать автором
Обновить

Комментарии 28

Остался один маленький шаг до изобретения конечного автомата...

Эх, к несчастью мы не можем детерминировать все возможные вариантов символов, которые пользователь может ввести например в то же поле ввода, а значит до конечного автомата никак не добраться(

Ну не обязательно же спускаться аж на уровень пользовательского ввода.


Вполне можно остаться на уровне состояний интерфейса. Вы это практически сделали в своем последнем примере. Осталось избавиться от флажков и явно прописать все возможные переходы.

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

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

    @Composable
    fun NamePreferences(data: UserPreferences){
        EditableText(
            text = data.name,
            onChange = { newValue ->
                data.name = newValue
            }
        )
    }

Осталось поверх этого MVI обвязать)

Насчет стейт машин, это все можно, только такая стейт машина называется state chart. В state charts есть бесконечный контекст который идет бок о бок с конечными состояниями и в контексте может быть что угодно.
Можешь почитать здесь: https://xstate.js.org/docs/about/concepts.html#statecharts

Спасибо, почитаю)

Есть лишь одна большая проблема комплексного состояния — мерж изменения. Если у нас поменялся условный флаг isLoading, вовсе не факт, что надо по другому отображать остальные поля. А значит, надо писать явную проверку на соответствие предыдущему отображения. Вот если каждое из свойств независимое, или сгруппировано по принципу конечного автомата — вопросов нет. Но принципиально пользоваться ровно одним состоянием, на мой взгляд, не всегда удобно
Соласен с вами, в данном подходе появляется такая проблема, но уже придумали несколько решений и можно выбирать то, которое подойдет лучше всего именно вам на проекте, или придумать свое, что-то новое и великое)
Не придерживаться парадигмы «одна VM — одно состояние». Собственно любой observe паттерн подойдет, и каноничный MVVM это никак не нарушит
Каноничный MVVM как раз имеет одно существенное отличие, которое было описано мной в одном из комментариев ниже)

У Вас получилось что-то очень похожее на Redux.

Название MVI, которое, вроде как, Hannes Dorfmann придумал в 2017, по сути и является недо-Redux-ом. Никогда не понимал зачем было это по-другому называть.

он его всего лишь популяризовал в среде Android, он сам писал, что упёр идею из мира JS-фреймворков

Ну, по своей сути этот подход к нам перекочевал как раз из фронтенд мира)
А MVPVM не решит тех проблем, что приведены в данной статье?
В описанных вами подходах, мы меньше контролируем то, что отображается на нашем ui.
Представьте вы стоите перед какой-то неведомой вам машиной и хотите, чтобы она что-то для вас сделала. Вам будет проще ошибиться если на ней будет 20 рубильников, которые можно включать и определенная их комбинация будет давать вам необходимый результат.
С другой стороны будет гораздо сложнее допустить ошибку, если вам дадут один терминал управления, в который можно будет отправить записку с тем результатом который вам нужен, а машина уже сама вам все покажет на основе этой записки.
Если брать в общем, то в этом как раз заключается ключевая разница между подходом MVI и остальными MV подходами

Кто по вашему должен следить за корректностью стейта?
Скажем в UI пришло: loading = true + isCreditDialogShowing = true, что по нашей логике не верно. Кто контролирует это?

В MVI, как такового контроля за этим мы не получаем, но, сам подход заставляет нас думать о том какие стейты будет иметь наш юай еще на стадии проектирования. Также мы всегда сможем проследить как стейт перетекает из одного, заглянув в наш стейт редьюсер (либо любую другую сущность, которая у нас отвечает за процессинг стейтов).
В случае MVP неконсистетное состояние на юай словить гораздо проще, в MVI же вас насторожит момент в котором при создании этого стейта оба флага loading и isCreditDialogShowing будут true.
К тому же MVI упрощает написание тестов, мы на какие-то инпуты ждем аутпут в виде стейта и проверяем его, если тест падает и приходит тот стейт, которого мы не ждем, то гораздо легче проследить, что пошло неправильно, так как по хорошему переход из одного стейта в другой происходит транзакционно.
Ну и исходя из практики, гораздо легче бывает дебажить, когда у нас во вью не миллион ручек, которые можно дергать, а всего одна и туда приходит стейт.

С диффингом изменений можно поступить так: в презентере заводим observable на какую то часть стейта, и применяем оператор distinctUntilChanged. Похоже на mvvm, но на самом деле single source of truth мы не теряем.

Как один из неплохих вариантов порешать проблему диффинга)
А почему бы не сравнить MVI с MVVM?
Тогда loading, isBannerShowing, isCreditDialogShowing из вашего примера были бы отдельными наблюдаемыми полями. А вместо processAction стало бы:

fun loadNames() {
    loading = true 
    isBannerShowing = false         
    isCreditDialogShowing = false
}

fun loadBanner() {
    loading = false 
    isBannerShowing = true         
    isCreditDialogShowing = false
}

fun loadCreditDialogInfo() {
    loading = false 
    isBannerShowing = false         
    isCreditDialogShowing = true
}

Чем это хуже?
При таком подходе я вижу проблему в том, что эти флаги по нашей логике отображения связаны и зависят друг от друга, но на деле мы получаем два наблюдаемых поля, которые в нашем коде не связаны абсолютно никак.
У нас приходит новый разраб, ему надо добавить третий флаг, он также делает его наблюдаемым полем. Пишет свою логику запроса и при успешном его завершении сетает в флаг true, но при этом ему наглядно нигде не увидеть какие еще флаги могут в этот момент быть и какие из них придется делать false.
В подходе со стейтами, ему придется добавить либо новый флаг в наш UI стейт, либо описать вообще новый стейт(зависит от подхода к реализации MVI), но в любом случае он уже на стадии проектирования задумается какое именно состояние экрана ему нужно, что должно показываться, а что нет, в таком подходе риск допустить ошибку снижается, ровно как и время ее исправления, если она все же была допущена. На моей практике был случай, когда на то, чтобы понять откуда на экране появляется лоадер уходило около недели, потому что во вью было много ручек и в какой последовательности они дергаются нигде детерминированно увидеть было невозможно, только дебаг и слезы
Получается, нам требуется, чтоб значения полей зависели друг от друга, и не было возможности эту зависимость сломать. В случае MVVM я вижу такое решение.
Нужно организовать состояние так, чтоб оно состояло из двух частей:
1) Основное состояние — оно изменяемое, его можно менять достаточно свободно, не нарушая при этом никаких зависимостей.
2) Производное состояние — напрямую менять его нельзя, оно пересчитывается автоматически как чистая функция от основного состояния.

Пример:
private var hasCreditOffer by state(false)
private var hasAvailableAdvertisement by state(false)
private var namesLoading by state(false)

val isCreditDialogShowing by computed { hasCreditOffer }

val isBannerShowing by computed {
    hasAvailableAdvertisement && !isCreditDialogShowing 
}

val progressShowing by computed {
    namesLoading && !isCreditDialogShowing && !isBannerShowing 
}

Похожие идеи можно увидеть в библиотеке MobX, которая достаточно популярна во фронтенд-мире.
О, а давайте выделим Ваши state'ы в отдельную сущность типа data класса? Ну, ведь их и десяток может быть, зачем держать их полями presenter'a / viewModel'и? Ой, получился mvi!
Довольно интересная идея, спасибо за комментарий, поизучаю MobX на досуге)
Немного поразмыслив про toast, мне захотелось к сущности UIState добавить еще сущность UICommand, которая должна выполниться только один раз. Ведь по сути, показ тоста и не влияет никак на состояние экрана, а полностью обрабатывается системой. Помимо показа тостов, это может быть, например, пересоздание активити при изменении языка/темы в настройках приложения (не девайса), или завершение активити (реализация кастомной кнопки «Выход» в приложении).
Вам пришла неплохая идея, нечто похожее реализовано в библиотеке MviCore от ребят их Badoo, можете посмотреть если вам интересно.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории