Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

Главная страница репозитория

Core MVI

Является развитием идей core-mvp и core-mvp-binding. Так же, этот подход во многом черпал вдохновение из Redux, Flux и MVI Android.

Внимание! Модуль находится в стадии активной разработки!

Общее описание

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

Ответственности классов

Event

Любое событие, обрабатываемое на UI-слое приложения. Пользовательский ввод, открытие экрана, загрузка данных, событие изменения сущности интерактора, и так далее. Удобнее всего эвенты одного экрана располагать в sealed class для ограничения их количества, и удобства навигации.

Реализации:

EventHub

Шина событий. Все события экрана проходят через нее. Является типизированной множеству событий экрана, задаваемому как Kotlin-sealed-class.

Благодаря этому, достигается явное ограничение множества событий (все события расположены в одном файле), отсекается возможность пропуска EventHub'ом определенных событий (событие невозможно поместить в EventHub, если оно не соответствует его типу).

Опционально: У одного экрана может быть несколько шин, и они все могут быть типизированы по разным множествам событий.

Таким образом, можно разбить экран на независимые друг от друга куски логики отображения.

Реализации:

  • EventHub - базовый типизированный хаб

  • RxEventHub - базовый хаб с поддержкой Rx

  • LifecycleEventHub - базовый хаб, который автоматически реагирует на события жизненного цикла экрана.

StateHolder

Класс, отвечающий за хранение состояния экрана, и его передачу View.

Reactor

Класс, осуществляющий реакцию на события и преобразование текущего состояния экрана. Содержит единственный метод react(holder, event), в котором обновляет поля у StateHolder в зависимости от пришедшего события.

Реализации:

Reducer

Класс, выполняющий изменение state в зависимости от события. Содержит единственный метод reduce(state, event): state, в котором с помощью текущего состояния экрана и события происходит получение нового состояния. Является расширением Reactor, и для работы требуется совместно реализовать класс ReducerStateHolder.

Реализации:

Middleware

Промежуточный слой между 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

Сущность, связывающая все остальные сущности вместе и осуществляющая подписку на шину сообщений. У одного экрана может быть только один Binder. Содержит методы для связи всех указанных выше сущностей для экранов с необходимостью отображения эвентов на UI, либо со связью только логики (EventHub, Middleware), когда отображение не требуется (например, SplashScreen).

Реализации:

Схематично, поток данных между всеми сущностями, описанными выше, выглядит следующим образом:

Data Flow Diagram

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

Linear Data Flow

Основные преимущества

Любые действие пользователя, вызовы методов жизненного цикла, получение данных с сервисного слоя, и отправка данных на 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

В проекте так же существует DSL-обертка над реактивным потоком данных, упрощающая взаимодействие с методом трансформаций в mvi. Подробнее можно почитать в readme.

Подключение

Для подключения данного модуля из Artifactory Surf необходимо, чтобы корневой build.gradle файл проекта был сконфигурирован так, как описано здесь.

Для подключения модуля через Gradle:

    implementation "ru.surfstudio.android:mvi-core:X.X.X"