Комментарии 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
У Вас получилось что-то очень похожее на Redux.
он его всего лишь популяризовал в среде Android, он сам писал, что упёр идею из мира JS-фреймворков
Представьте вы стоите перед какой-то неведомой вам машиной и хотите, чтобы она что-то для вас сделала. Вам будет проще ошибиться если на ней будет 20 рубильников, которые можно включать и определенная их комбинация будет давать вам необходимый результат.
С другой стороны будет гораздо сложнее допустить ошибку, если вам дадут один терминал управления, в который можно будет отправить записку с тем результатом который вам нужен, а машина уже сама вам все покажет на основе этой записки.
Если брать в общем, то в этом как раз заключается ключевая разница между подходом MVI и остальными MV подходами
Кто по вашему должен следить за корректностью стейта?
Скажем в UI пришло: loading = true + isCreditDialogShowing = true, что по нашей логике не верно. Кто контролирует это?
В случае MVP неконсистетное состояние на юай словить гораздо проще, в MVI же вас насторожит момент в котором при создании этого стейта оба флага loading и isCreditDialogShowing будут true.
К тому же MVI упрощает написание тестов, мы на какие-то инпуты ждем аутпут в виде стейта и проверяем его, если тест падает и приходит тот стейт, которого мы не ждем, то гораздо легче проследить, что пошло неправильно, так как по хорошему переход из одного стейта в другой происходит транзакционно.
Ну и исходя из практики, гораздо легче бывает дебажить, когда у нас во вью не миллион ручек, которые можно дергать, а всего одна и туда приходит стейт.
С диффингом изменений можно поступить так: в презентере заводим observable на какую то часть стейта, и применяем оператор distinctUntilChanged. Похоже на mvvm, но на самом деле single source of truth мы не теряем.
Тогда 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), но в любом случае он уже на стадии проектирования задумается какое именно состояние экрана ему нужно, что должно показываться, а что нет, в таком подходе риск допустить ошибку снижается, ровно как и время ее исправления, если она все же была допущена. На моей практике был случай, когда на то, чтобы понять откуда на экране появляется лоадер уходило около недели, потому что во вью было много ручек и в какой последовательности они дергаются нигде детерминированно увидеть было невозможно, только дебаг и слезы
Нужно организовать состояние так, чтоб оно состояло из двух частей:
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, которая достаточно популярна во фронтенд-мире.
Так для чего же нам все таки нужен MVI в мобильной разработке