Android mvi что это

MVIDroid: обзор новой библиотеки MVI (Model-View-Intent)

Всем привет! В этой статье я хочу рассказать о новой библиотеке, которая привносит шаблон проектирования MVI в Android. Эта библиотека называется MVIDroid, написана 100% на языке Kotlin, легковесная и использует RxJava 2.x. Автор библиотеки лично я, исходный код её доступен на GitHub, а подключить её можно через JitPack (ссылка на репозиторий в конце статьи). Эта статья состоит из двух частей: общее описание библиотеки и пример её использования.

И так, в качестве предисловия, позвольте напомнить что такое вообще MVI. Model — View — Intent или, если по-русски, Модель — Представление — Намерение. Это такой шаблон проектирования, в котором Модель (Model) является активным компонентом, принимающим на вход Намерения (Intents) и производящая Состояния (State). Представление (View) в свою очередь принимает Модели Представления (View Model) и производит те самые Намерения. Состояние преобразуется в Модель Представления при помощи функции-трансформера (View Model Mapper). Схематически шаблон MVI можно представить следующим образом:

В MVIDroid Представление не производит Намерения напрямую. Вместо этого оно производит События Представления (UI Events), которые затем преобразуются в Намерения при помощи функции-трансформера.

Основные компоненты MVIDroid

Модель

Начнём с Модели. В библиотеке понятие Модели немного расширено, здесь она производит не только Состояния но и Метки (Labels). Метки используются для общения Моделей между собой. Метки одних Моделей могут быть преобразованы в Намерения других Моделей при помощи функций-трансформеров. Схематически Модель можно представить так:

В MVIDroid Модель представлена интерфейсом MviStore (название Store заимствовано из Redux):

И так, что мы имеем:

  • Интерфейс имеет три Generic-параметра: State — тип Состояния, Intent — тип Намерений и Label — тип Меток
  • Содержит три поля: state — текущее состояние Модели, states — Observable Состояний и labels — Observable Меток. Последние два поля дают возможность подписаться на изменения Состояния и на Метки соответственно.
  • Является потребителем (Consumer) Намерений
  • Является Disposable, что даёт возможность разрушить Модель и прекратить все происходящие в ней процессы

Обратите внимание, что все методы Модели должны выполняться на главном потоке. То же самое справедливо и для любого другого компонента. Выполнять фоновые задачи, разумеется, можно используя стандартные средства RxJava.

Компонент

Компонент в MVIDroid — это группа Моделей, объединённых общей целью. Например можно выделить в Компонент все Модели для какого-либо экрана. Иными словами, Компонент является фасадом для заключённых в него Моделей и позволяют скрыть детали реализации (Модели, функции-трансформеры и их связи). Давайте посмотрим на схему Компонента:

Как видно из схемы, компонент выполняет важную функцию преобразования и перенаправления событий.

Полный список функции Компонента выглядит следующим образом:

  • Связывает входящие События Представлений и Метки с каждой Моделью используя предоставленные функции-трансформеры
  • Выводит исходящие Метки Моделей наружу
  • Разрушает все Модели и разрывает все связи при разрушении Компонента

Компонент тоже имеет свой интерфейс:

Читайте также:  Как обновить версию андроида с galaxy tab

Рассмотрим интерфейс Компонента подробнее:

  • Содержит два Generic-параметра: UiEvent — тип Событий Представления и States — тип Состояний Моделей
  • Содержит поле states, дающее доступ к группе Состояний Моделей (например в виде интерфейса или data-класса)
  • Является потребителем (Consumer) Событий Представления
  • Является Disposable, что даёт возможность разрушить Компонент и все его Модели

Представление (View)

Как несложно догадаться, Представление нужно для отображения данных. Данные для каждого Представления группируются в Модель Представления (View Model) и обычно представляются в виде data-класса (Kotlin). Рассмотрим интерфейс Представления:

Здесь всё несколько проще. Два Generic-параметра: ViewModel — тип Модели Представления и UiEvent — тип Событий Представления. Одно поле uiEvents — Observable Событий Представления, дающее возможность клиентам подписаться на эти самые события. И один метод subscribe(), дающий возможность подписаться на Модели Представления.

Пример использования

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

Представление

Для начала опишем Модель Представления:

И События Представления:

Теперь реализуем само Представление, для этого нам понадобится наследование от абстрактного класса MviAbstractView:

Всё предельно просто: подписываемся на изменения UUID и обновляем TextView при получении нового UUID, а по нажатию кнопки отправляем событие OnGenerateClick.

Модель

Модель будет состоять из двух частей: интерфейс и реализация.

Здесь всё просто: наш интерфейс расширяет интерфейс MviStore, указывая типы Состояния (State) и Намерений (Intent). Тип Меток — Nothing, т. к. у наша Модель их не производит. Также в интерфейсе содержатся классы Состояния и Намерений.

Для того что реализовать Модель, надо понять как она работает. На вход Модели поступают Намерения (Intent), которые преобразуются в Действия (Action) при помощи специальной функции IntentToAction. Действия поступают на вход Исполнителю (Executor), который выполняет их и производит Результаты (Result) и Метки (Label). Результаты затем поступают в Редуктор (Reducer), который преобразует текущее Состояние в новое.

Все четыре состовляющие Модели:

  • IntentToAction — функция, преобразующая Намерения в Действия
  • MviExecutor — исполняет Действия и производит Результаты и Метки
  • MviReducer — преобразует пары (Состояние, Результат) в новые Состояния
  • MviBootstrapper — специальный компонент, позволяющий инициализировать Модель. Выдаёт всё те же Действия, которые также поступают в Исполнитель (Executor). Можно выполнить разовое Действие, а можно подписаться на источник данных и выполнять Действия при определённых событиях. Bootstrapper запускается автоматически при создании Модели.

Чтобы создать саму Модель, необходимо использовать специальную фабрику Моделей. Она представлена интерфейсом MviStoreFactory и его реализацией MviDefaultStoreFactory. Фабрика принимает составляющие Модели и выдаёт готовую к использованию Модель.

Фабрика нашей Модели будет выглядеть следующим образом:

В этом примере представлены все четыре составляющие Модели. Сначала фабричный метод create, затем Действия и Результаты, за ними следует Исполнитель и в самом конце Редуктор.

Компонент

Состояния Компонента (группа Состояний) опишем data-классом:

При добавлении новых Моделей в Компонент, их Состояния следует также добавить в группу.

Читайте также:  Сканер блютуз для андроида

И, собственно, сама реализация:

Мы наследовали абстрактный класс MviAbstractComponent, указали типы Состояний и Событий Представления, передали нашу Модель в super класс и реализовали поле states. Кроме того мы создали функцию-трансформер, которая будет преобразовывать События Представления в Намерения нашей Модели.

Маппинг Модели Представления

У нас есть Состояния и Модель Представления, настало время преобразовать одно в другое. Для этого мы реализуем интерфейс MviViewModelMapper:

Связь (Binding)

Наличия самих по себе Компонента и Представления не достаточно. Чтобы всё начало работать, их необходимо связать. Пришло время создать Activity:

Мы использовали метод bind(), который принимает Компонент и массив Представлений с мапперами их Моделей. Этот метод является extension-методом над LifecycleOwner (коими являются Activity и Fragment) и использует DefaultLifecycleObserver из пакета Arch, который требует Java 8 source compatibility. Если по каким-либо причинам Вы не можете использовать Java 8, то Вам подойдёт второй метод bind(), который не являеся extension-методом и возвращает MviLifecyleObserver. В этом случае, Вам придётся вызывать методы жизненного цикла самостоятельно.

Ссылки

Исходный код библиотеки, а также подробную инструкцию по подключению и использованию можно найти на GitHub.

Источник

Приручая MVI

О том, как распутать джунгли MVI, используя Джунгли собственного производства, и получить простое и структурированное архитектурное решение.

Предисловие

Впервые наткнувшись на статью о Model-View-Intent (MVI) под Android, я даже не открыл ее.
— Серьезно!? Архитектура на Android Intents?

Это была глупая идея. Намного позже я прочел про MVI и узнал, что главным образом данная архитектура сосредоточена на однонаправленных потоках данных и управлении состояния.

Изучая MVI, я невольно столкнулся с проблемой, что весь подход выглядит как-то запутанно, как какие-то дебри. Да, на выходе получается решение с плюсами по отношению к MVP и MVVM, но, смотря на эту всю комплексность, задаешься вопросом: «А стоило ли?».

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

Так я решил написать свое решение. Основные требования (по значимости):

  1. Простое;
  2. Покрывает все UI кейсы, которые я только могу придумать;
  3. Структурированное.

И что это?

Позвольте представить — Джунгли (Jungle). Под капотом — только RxJava с ее реактивным подходом.


Все отношения, показанные на картинке, — опциональны

Как это работает?

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

  1. Показывать PrgoressBar во время загрузки;
  2. Отображать Button для перезагрузки списка и Toast с сообщением об ошибке в случае ошибки;
  3. Если страны были успешно загружены, отображать список стран;
  4. Попробовать загрузить страны на открытии окна автоматически, без каких-либо действий пользователя.

Давайте напишем нашу UI часть:

Что из этого (пока) можно понять? Мы можем послать DemoEvent.Load нашему DemoStore (по клику на Reload кнопку); получить DemoAction.ShowError (с данными об ошибке) и отобразить Toast; получить обновление по DemoState (с данными о странах и состоянии загрузки) и отобразить UI компоненты в соответствии с требованиями. Вроде бы не так уж и сложно.

Читайте также:  Asus zenfone 2 laser ze500kl обновление до android 7

Теперь приступим к нашему DemoStore. В первую очередь, унаследуем его от Store, разрешим получать DemoEvent, производить DemoAction и изменять DemoState:

Затем, создадим CountryMiddleware, который будет ответственным за предоставление данных о загрузке стран:

Что такое Command? Это специфичный сигнал, который побуждает «что-то» сделать. А CommandResult? Это результат выполнения этого «чего-то».

В нашем случае CountryMiddleware.Input сигнализирует, что логика CountryMiddleware должна быть выполнена. Каждое выполнение логики Middleware возвращает CommandResult; для лучшей структуры приложения можно хранить этот результат внутри sealed класса (CountryMiddleware.Output).

В нашем случае мы попросту возвращаем Observable, который испустит Output.Loading во время загрузки, Output.Loaded с данными на успешную загрузку, Output.Failed с информацией об ошибке на ошибку.

Давайте вернемся к DemoStore и заставим обработать CountryMiddleware на нажатие Reload кнопку:

Переопределяя поле middlewares мы указываем, какие Middlewares наш DemoStore может обработать. Под капотом Store использует Commands. Поэтому нам следует сконвертировать наш DemoEvent.Load в CountryMiddleware.Input (для того, чтобы принудить перезагрузку).

Итак, теперь мы можем получать результат от CountryMiddleware. Давайте позволим последнему изменять наш DemoState:

Прежде чем изменять State, необходимо указать его начальное состояние в initialState . После этого в методе reduceCommandResult описывается логика того, как каждый CommandResult изменяет State.

Для отображения ошибки загрузки используется DemoAction.ShowError. Чтобы сгенерировать последний, необходимо предоставить новую Command (из CommandResult) и связать ее с нашим Action:

Последнее, что осталось сделать — привязать автоматический запуск выполнения CountryMiddleware. Все, что нужно сделать, это добавить его Command в bootstrapCommands :

Просто?

Можно использовать конкретно только то, что вам нужно, без какой-либо лишней логики. Несколько классов и щепотка магии под капотом. Один Store, опционально несколько Middlewares, опционально имплементация MviView.

Ваша View должна только отображать обновления какой-то функциональности бизнес логики? Вам даже не нужны Events, только Store, Middleware и переопределение метода render функции в MviView.

Только кнопка, по клику которой происходит какая-то навигация? Окей, стоит только поиграться с Event внутри Store и ничего больше.

Как мне кажется, это простое решение, так как требует небольших усилий как для понимания, так и для использования.

Структурировано?

Для того, чтобы поддерживать структурированность, необходимо:

  • Хранить Commands в sealed классах внутри Store, группируя их по назначения: генерирующие Actions или напрямую изменяющие State?
  • Хранить Commands, относящихся к Middlewares, внутри последних.

Также стоит помнить, что Middleware — про одну функциональность, что делает его похожим на UseCase (Interactor). На мой взгляд, присутствие последнего (и, как следствие, какого-то domain layer) говорит о хорошо структурированном проекте. По этой же аналогии, я считаю, что использование Middleware способствует улучшению структуры проекта.

Заключение

С использованием Джунгей у меня есть четкое представление того, как организуется навигация внутри подхода. Я также уверен, что проблема SingleLiveEvent может быть легко разрешена с использованием Actions.

Более подробные разборы работы можно найти на wiki. Отвечу на любые вопросы. Буду рад, если вам данное решение покажется полезным!

Источник

Оцените статью