Является развитием идей core-mvp и core-mvp-binding. Так же, этот подход во многом черпал вдохновение из Redux, Flux и MVI Android.
Внимание! Модуль находится в стадии активной разработки!
Подход заключается в полном абстрагировании сущностей UI-слоя друг от друга, и в выстраивании непрямых связей между ними на основе отсылки событий. Таким образом, получается решить проблему излишней запутанности связей между элементами и наладить полностью однонаправленный поток данных.
Любое событие, обрабатываемое на UI-слое приложения. Пользовательский ввод, открытие экрана, загрузка данных, событие изменения сущности интерактора, и так далее. Удобнее всего эвенты одного экрана располагать в sealed class для ограничения их количества, и удобства навигации.
Реализации:
Шина событий. Все события экрана проходят через нее. Является типизированной множеству событий экрана, задаваемому как Kotlin-sealed-class.
Благодаря этому, достигается явное ограничение множества событий (все события расположены в одном файле), отсекается возможность пропуска EventHub'ом определенных событий (событие невозможно поместить в EventHub, если оно не соответствует его типу).
Опционально: У одного экрана может быть несколько шин, и они все могут быть типизированы по разным множествам событий.
Таким образом, можно разбить экран на независимые друг от друга куски логики отображения.
Реализации:
-
EventHub - базовый типизированный хаб
-
RxEventHub - базовый хаб с поддержкой Rx
-
LifecycleEventHub - базовый хаб, который автоматически реагирует на события жизненного цикла экрана.
Класс, отвечающий за хранение состояния экрана, и его передачу View.
Класс, осуществляющий реакцию на события и преобразование текущего состояния экрана. Содержит единственный метод react(holder, event),
в котором обновляет поля у StateHolder в зависимости от пришедшего события.
Реализации:
Класс, выполняющий изменение state в зависимости от события.
Содержит единственный метод reduce(state, event): state, в котором с помощью текущего состояния экрана и события происходит получение нового состояния. Является расширением Reactor, и для работы требуется совместно реализовать класс ReducerStateHolder.
Реализации:
Промежуточный слой между UI и данными. Является аналогом SideEffect из Redux-терминологии.
Middleware принимает в себя поток событий, трансформирует его произвольным образом, и направляет поток новых событий в EvetHub.
В терминологии core-mvp, является аналогом презентера, который, например, в качестве реакции на события нажатия на кнопку reloadBtn, перезагружает данные, однако имеет более декларативный синтаксис:
Observable<Event.Reload> -> Observable<Event.LoadData<T>>
Чаще всего используется для получения данных из сервисного слоя, реакции на события ЖЦ, открытия экранов и подобных действий.
Может использоваться для комбинации двух состояний. Так как вызов метода Middleware.transform выполняется после выполнения react у Reactor/Reducer, данные, внутри transform всегда будут актуальными.
Например, у кнопки , необходимо проставить isEnabled в зависимости от валидации некоторых полей. Поля изменяются с помощью событий Event.FieldChanged. Таким образом, при получении этого события в методе transform, Reactor уже отреагирует на событие и изменит модель, и мы сможем без проблем задействовать обновленную модель в Middleware, провести валидацию там, и вывести в выходящий поток Event.IsDataValid(boolean)
Реализации:
-
Middleware - базовый типизированный Middleware
-
RxMiddleware - базовый Middleware с поддержкой Rx
Сущность, связывающая все остальные сущности вместе и осуществляющая подписку на шину сообщений. У одного экрана может быть только один Binder. Содержит методы для связи всех указанных выше сущностей для экранов с необходимостью отображения эвентов на UI, либо со связью только логики (EventHub, Middleware), когда отображение не требуется (например, SplashScreen).
Реализации:
Схематично, поток данных между всеми сущностями, описанными выше, выглядит следующим образом:
Если попытаться представить поток данных линейно (от действия пользователя к загрузке данных из сети и отображению на экране данных, они будут выглядеть так:
Любые действие пользователя, вызовы методов жизненного цикла, получение данных с сервисного слоя, и отправка данных на UI рассматриваются как единая сущность: событие.
Все события, совершаемые на всех стадиях жизни экрана, проходят через единую шину: хаб.
Таким образом, достигается полная абстракция классов внутри экрана, и устранение связей между ними.
К тому же, благодаря единой точке входа и говорящему именованию, подход открывает возможность к очень легкому и действенному логированию всего, что в данный момент происходит на UI-слое приложения.
Важную роль играет разделение ответственностей между классами. Если в каноничном MVP Presenter отвечал и за управление подписками, и за хранение, и за трансформацию данных, в данном подходе было решено разделить его на независимые части.
Так же, важную роль играет сохранение плюсов подхода реактивных биндингов: core-mvp-binding.
Модель View в каноничном виде MVI всегда отражает полное состояние экрана, и при любом изменении модели требуется полная перерисовка экрана. Этот подход реализован с помощью
State Reducer Pattern.
Однако из-за перерисовки экрана при получении каждого нового события, может значительно страдать производительность.
Для того, чтобы этого избежать, вместо хранения в модели всех данных в простом виде, задействуются сущности State и Command из вышеописанного модуля: core-mvp-binding. Благодаря им, View может вместо подписки на изменение экрана целиком, подписаться на изменение значений всех переменных, и перерисовыватьтолько небольшие части экрана, зависящие от этих переменных. Этот подход получил название State Reactor.
Базовые реализации классов вынесены из модуля для поддержания гибкости. Вы можете найти примеры реализаций в модуле core-mvi-sample, в папке ui/base
Для упрощения представления цикла загрузки данных из сервисного слоя была выделена сущность Request. Подход с ней состоит в оборачивании Observable<T> в Observable<Request<T>>.
При этом Request<T> содержит 3 состояния:
-
Loading- данные загружаются из сервисного слоя -
Success<T>- данные пришли с сервисного слоя -
Error- ошибка загрузки данных
В один момент времени Request может содержать только одно значение: либо Loading, либо Data, либо Error.
Основные сущности этого подхода:
-
Request - реализация Request в виде sealed-класса.
-
RequestEvent - событие загрузки данных
-
RequestUi - состояние загрузки данных в удобной для отображения на Ui форме.
В отличие от Request, содержащего только состояние запроса в текущий момент времени (загрузка или данные или ошибка) этот класс содержит комбинацию из них (загрузка и данные и ошибка). Кроме того, содержит информацию о том, как именно состояние загрузки данных должно быть отображено на Ui:
- Loading - интерфейс-обертка над состоянием загрузки данных (аналогично LoadStateInterface). Необходим для удобного отображения состояния загрузки на Ui.
-
RequestState - Ui-
Stateасинхронной загрузки данных. Содержит в себе неизменяемый экземплярRequestUi, и методы для удобного доступа к данным.
В проекте так же существует DSL-обертка над реактивным потоком данных, упрощающая взаимодействие с методом трансформаций в mvi. Подробнее можно почитать в readme.
Для подключения данного модуля из Artifactory Surf
необходимо, чтобы корневой build.gradle файл проекта был сконфигурирован так,
как описано здесь.
Для подключения модуля через Gradle:
implementation "ru.surfstudio.android:mvi-core:X.X.X"

