- Общее описание
- Основные преимущества
- Ответственности классов
- DSL
- Вычисление диффов при отрисовке состояния
- Пример
- Roadmap
Современная библиотека для организации слоя представления. Является продолжением и развитием идей библиотеки Surf-MVI Подход во многом черпал вдохновение из Redux, Flux и MVI Android.
Внимание!
Модуль находится в стадии активной разработки!
Основная идея в том, что у экрана есть единое состояние. А логика изменения этого состояния отделена от логики запросов в сеть или базу данных. Изменять состояние экрана не может никто, кроме Reducer. Ответственность за трансформацию потока событий лежит на Middleware. Таким образом, вся логика, приводящая к изменению состояния UI, концентрируется в одном месте, и она отделена от логики запросов к серверу, в базу данных, получению каких-то обновлений с других экранов и тд. А вью остается только отрисовывать это состояние и отправлять события взаимодействия с пользователем.
Любые действия пользователя, вызовы методов жизненного цикла, получение данных с сервисного слоя и отправка данных на UI рассматриваются как единая сущность: событие
.
Все события, совершаемые на всех стадиях жизни экрана, проходят через единую шину: хаб
.
Таким образом, достигается полная абстракция классов внутри экрана, и устранение связей между ними.
К тому же, благодаря единой точке входа и говорящему именованию, подход открывает возможность к очень легкому и действенному логированию всего, что в данный момент происходит на UI-слое приложения.
Важную роль играет разделение ответственностей между классами. Если в каноничном MVP Presenter отвечал и за управление подписками, и за хранение, и за трансформацию данных, в данном подходе было решено разделить его на независимые части.
Модель View
в каноничном виде MVI всегда отражает полное состояние экрана, и при любом изменении модели требуется полная перерисовка экрана. Этот подход реализован с помощью
State Reducer Pattern.
Однако из-за перерисовки экрана при получении каждого нового события, может значительно страдать производительность. Это решается при помощи Вычисления диффов при отрисовке состояния
В текущей реализации библиотеки сущности и их ответственности остались теми же, как в предыдущей версии, в которой использовалась RxJava.
- State Состояние экрана. Содержит всю необходимую информацию для того, чтобы отрисовать вью. Для хранения Observable состояния используется обертка StateHolder.
- Event
Событие, происходящее на экране. Клик пользователя, получение данных из базы, запрос к серверу - все это описывается событиями.
В Surf-MVI по умолчанию нет строгого разделения событий на события, источником которых был UI (например клик) или события, полученные в результате взаимодействия с сервисным слоем (например при получении ответа на запрос к серверу). Группировка событий остается на усмотрение разработчика с помощью
sealed class
. - EventHub Шина для отправки и получения событий. Все события, которые эмитит UI или сервисный слой, оказываются в этой шине.
- Reducer Отвечает за изменение текущего состояния (и отправку сайд-эффектов, о них чуть позже). Каждый раз, когда какое-то событие оказывается в EventHub’е, оно прилетает в редьюсер, который должен решить, нужно ли как-то изменить состояние. Редьюсер, и только редьюсер, может менять состояние вью.
- Middleware Трансформирует поток событий из EventHub’а в поток новых событий. Например событие клика по кнопке в событие начала загрузки данных и после этого ошибки или успешного окончания загрузки.
- ScreenBinder Связывает все остальные сущности для совместного взаимодействия. События EventHub’а отправляются в Middleware для трансформации, в то же время Reducer получает уведомление о каждом событии, которое попадает в EventHub.
Одной из главных фишек библиотеки Surf-MVI был DSL, позволяющий описывать трансформации событий в лаконичном стиле используя средства котлина.
mvi-flow уже подерживает почти все DSL-трансформации, которые поддерживала предыдущая версия
Подробнее про DSL можно почитать в DSL
Если мы используем Android View Framework и просто подпишемся на Observable состояние экрана и будем каждый раз полностью его перерисовывать, когда получим новое состояние, то на экранах с большим количеством логики и сложными вью может упасть FPS. И чем чаще будет обновляться состояние, тем сильнее будет падение.
Для этого было сделано простое решение: перерисовывать только те части экрана, которые поменялись или отрисовываются в первый раз.
На проектах для этого использовалась утилитарная функция actionIfChanged
или одна из ее перегрузок для разного количества аргументов (обычно больше четырех для одной вью не трубуется, а для особых случаев можно сделать обертку).
fun <T : View, R1, R2, R3, R4> T.actionIfChanged(
data1: R1?,
data2: R2? = null,
data3: R3? = null,
data4: R4? = null,
action: T.(data1: R1?, data2: R2?, data3: R3?, data4: R4?) -> Unit) {
val hash = data1?.hashCode() ?: 0
.plus(data2?.hashCode() ?: 0)
.plus(data3?.hashCode() ?: 0)
.plus(data4?.hashCode() ?: 0)
if (this.tag != hash) {
action(data1, data2, data3, data4)
this.tag = hash
}
}
При использовании Jetpack Compose дополнительные действия не потребуются, так как в Compose подсчет Diff есть из коробки и Compose отлично подходит под работу с единым состоянием.
В модуле sample можно посмотреть две реализации с полным набором сущностей.
- Простая с примерами использования трансформаций и тривиальной логикой экрана
- С запросом в сеть и обработкой ошибок с помощью ErrorHandler (используется на большинстве проектов).