Когда старые инструменты не соответствуют растущей сложности программ, появляются новые подходы к программированию, шаблоны проектирования. MVI помогает там, где подходы MVP и MVVM терпят неудачу. Сейчас, в том числе, этот подход активно используется во многих крупных приложениях. В одних случаях MVI используется только на части экранов, в других на нем строятся все экраны. В нашей команде знание МВИ является одним из обязательных требований.

В этой статье мы разберем решения для реализации архитектуры MVI на Android, большинство из которых применимы в работе. Они описали свои преимущества и недостатки, опираясь на опыт коллег и собственную практику. Материал будет полезен разработчикам, еще не знакомым с паттерном MVI, и тем, кто выбирает библиотеку для своего проекта.

Общая схема МВИ

Давайте проанализируем паттерн MVI (Model-View-Intent). Первоначально он был разработан для фреймворка Cycle.js Андре Штальтцем.

Разберем принцип работы подробнее. Например, мы находимся на экране и хотим отправить какое-то событие. Intent получит это событие и преобразует его в необходимые данные. Модель будет обрабатывать эти новые данные, например делать запрос на сервер, и возвращать новую, готовую к визуализации модель, которую может отображать представление.

Например, мы можем взять отправку сообщения в чате. Пусть в нашем случае событием будет клик из View. Затем Intent преобразует этот щелчок в действие для отправки моделью нового сообщения. Модель обработает эти данные, отправится на сервер и вернет нам новую модель. Представление покажет нам, что новое сообщение было успешно отправлено.

Почему удобно работать с МВИ?

  • Однонаправленный . С данными работает только одна сущность, и мы знаем, кто меняет данные, зачем и зачем.
  • Неизменяемость состояния . Новое состояние состоит из предыдущего и некоторого действия. Мы не можем изменить данные, мы можем только получить новые.

Эти два пункта составляют один главный пункт — у нас есть единственный источник правды. Полученные данные неизменны, и мы знаем, кто за них отвечает.

Вышеприведенные пункты относятся не только к MVI, но и к архитектуре однонаправленного потока данных в целом.

Есть и другие преимущества:

  • Простота регистрации и отладки. Легко воспроизвести, где была ошибка, и собрать все условия.

Этот пункт включает понятие Отладка путешествия во времени. Например, после краша у пользователя мы можем отследить в Crashlytics его текущее состояние и то, которое было до краха.

  • Простота тестирования — компоненты можно тестировать независимо друг от друга.
  • Безопасность потоков . Есть в большинстве фреймворков — с данными работает только одна сущность.
  • С Jetpack Compose удобно работать.

Также обратите внимание, что MVI применим не только для веб-платформ или Android-платформ. Например, для проектов на Android и iOS можно брать одинаковые решения. Но в этой статье мы рассмотрим только библиотеки, используемые на Android.

Разновидности MVI в Android

Мы предлагаем обратиться к различным реализациям MVI. Мы расскажем о самых интересных и удобных вариантах и ​​о тех, которые заслуживают внимания в контексте истории развития подхода.

Редукс

Фреймворк Java Script, написанный Даниилом Абрамовым и Эндрю Кларком в 2015 году.

В Redux есть понятие State или State Tree — само состояние или само дерево состояний. Так же есть Store — хранилище этих состояний. От этого компонента мы можем запросить текущее состояние, подписаться на изменения или изменить состояния. Вы можете обновить состояние с помощью функции отправки, передав туда действие. Внутри диспетчеризации будет вызвана функция редьюсера, и новое состояние будет сохранено. И мы можем получить обновленное состояние, используя метод getState.

Мосби Ханнеса Дорфманна

Пример реализации

override fun bindIntents() {
    val viewStateObservable = intent(SomeView::someIntent)
                           .switchMap(::doSomethingAndReturnState)
    subscribeViewState(viewStateObservable, SomeView::render)
}

Библиотека была создана на базе RxJava в 2016 году и впервые была представлена ​​в серии статей Ханнеса Дорфмана в 2017 году. Это первая библиотека MVI для Android, она легко ложится на классическую схему MVI. Но сейчас он не так интересен с точки зрения разработки продукта и не поддерживается, поэтому подробно на нем останавливаться не будем.

MVICore от Badoo

Эта библиотека известна многим и наверняка кто-то из читателей ей пользовался. Главный компонент, внутри которого скрыта логика работы — это Feature. Можем послать ему Wish — скажем, что хотим совершить какое-то действие. После его обработки фича может возвращать либо новое состояние (которое мы потом покажем пользователю), либо новости (аналогично SingleLiveEvent).

Как это происходит внутри Feature? Само Желание обрабатывается в Актере, объекте, который управляет основной логикой. Актер отвечает за асинхронные задачи, такие как запросы к серверу. Результат Актер — Эффект будет обработан в Редьюсере. Редуктор отвечает за создание нового состояния на основе предыдущего состояния и полученных данных. Если у нас нет Актера, Wish будет обрабатываться Редюсером.

При необходимости мы можем отслеживать изменения состояния и отправлять события, которые будут выполняться только один раз. В этом случае мы можем использовать сущность NewsPublisher, которая может создавать такие события — News.

Если мы хотим отправить Wish внутри Feature, нам нужен компонент Bootstrapper. Например, он может запустить какой-то запрос, когда мы только что зашли на экран.

PostProcessor может помочь, когда нужно отправить новое Wish по результатам обработки предыдущих событий, например, повторно отправить запрос после ошибки.

Плюсы:

  1. Масштабируемость. Вы можете использовать только Редуктор. Но в базовой реализации, скорее всего, будет минимально использоваться Reducer + Actor.
  2. Поддержка SingleLiveEvent.
  3. Удобно тестировать как весь компонент Feature, так и отдельные компоненты.
  4. Не применяет какую-либо конкретную архитектуру, обеспечивает очень гибкое использование. Вы можете повторно использовать Feature для нескольких экранов.
  5. Логин и отладка путешествий во времени.
  6. Плагин для IDEA.
  7. Базовые классы для поддержки Android, но не полностью привязаны к библиотеке.

Минусы:

  1. Трудно понять.
  2. Нет поддержки Kotlin Multiplatform.
  3. Только RxJava.

МВИКотлин Аркадия Иванова

Вдохновлен библиотекой MVICore и в целом похож на нее. Возможно, вы слышали о MVIKotlin под старым названием MVIDroid — название было изменено после добавления поддержки Kotlin Multiplatform.

Основным компонентом библиотеки является Магазин. Мы можем получить к нему доступ с помощью Intent. После обработки Intent наружу мы получаем Label (аналогично SingleLiveEvent) и State.

Вместо Актера MVIKotlin использует компонент Executor. Его основное отличие в том, что он может сам отправлять SingleLifeEvent.

Bootstrapper также имеет некоторые отличия. Он отправляет события, отличные от Intent (которые приходят извне), поэтому мы сразу понимаем, откуда пришло событие.

Плюсы:

  1. Масштабируемость.
  2. Поддержка SingleLiveEvent.
  3. Поддержка мультиплатформы Kotlin.
  4. Есть версия как в RxJava, так и в сопрограммах.
  5. Не применяет какую-либо конкретную архитектуру, обеспечивает очень гибкое использование. Вы можете повторно использовать Магазин для нескольких экранов.
  6. Логин и отладка путешествий во времени.
  7. Плагин для IDEA и Android Studio.

Минусы:

  1. Не очень легко понять.
  2. Могут быть сложности с использованием Koin из-за жизненного цикла контроллера (сущность, которая связывает все вместе). Подробнее об этом написано в статье.

Подробнее прочитать о MVIKotlin и посмотреть примеры кода можно здесь.

Подход Redux также используется в библиотеках Roxy, Redux-Kotlin, EBA и других.

МВВМ+

Подход MVVM+ появился в 2020 году. Его название объясняется тем, что он больше похож на MVVM, чем на классический MVI, и сочетает в себе оба решения. Важным отличием MVVM+ является то, что у каждого Intent есть свой Transformer и Reducer. Таким образом, они будут выполняться для каждого события и после этого объединяться в общее состояние.

Орбита Мэтью Долана и Миколая Лещинского

В этой библиотеке мы ссылаемся на сущность ContainerHost. Чтобы получить к нему доступ, мы просто делаем вызовы, и после некоторой работы мы получаем Side Effect (аналогично SingleLiveEvent) и State.

Эта библиотека сильно отличается от предыдущих. Когда мы получаем вызов ContainerHost, он будет передан в контейнер. В свою очередь Контейнер может либо выдать Side Effect, либо выполнить какое-то преобразование (например, перейти на сервер) и передать событие на Reducer. Редуктор уже сгенерирует новое состояние.

Пример обновления статуса

host.intent {
       reduce {
        state.copy(...)
    }
}

Пример отправки побочных эффектов

host.intent {
    postSideEffect(SideEffect())
}

Плюсы:

  1. Низкий порог входа.
  2. Легко использовать любой DI.
  3. Поддержка SingleLiveEvent.
  4. Есть поддержка мультиплатформенности Kotlin.
  5. Написан с использованием сопрограмм (это может быть как плюс, так и минус).
  6. Есть сохранение состояния из коробки.
  7. Есть дополнительная библиотека для удобства написания юнит-тестов.

ConsL

  1. Нет удобного логирования и отладки во времени. Но разработчики планируют добавить его в будущем.

Также подход MVVM+ используется в библиотеках Uniflow и Mavericks.

Архитектура Вяз (ЧЭА)

Он имеет определенное сходство с MVI, хотя на самом деле не имеет к нему отношения. Это архитектура языка Elm, созданная в 2012 году.

Если MVI — это Model-View- Intent (от намерения — «‎намерение»), то ELM — это Model-View- Update (обновление — «‎update»). В случае MVI логика распределяется между Reducer и Intent, а в случае ELM вся логика находится в Update. Рассмотрим это подробнее на примере ниже.

Элмсли от Vivid

В библиотеке есть основное хранилище сущностей, к которому мы обращаемся через событие — Event.UI. Снаружи мы получим Эффект (SingleLiveEvent), Состояние или и то, и другое.

Рассмотрим подробнее компоненты библиотеки. Например, мы получили какой-то Event.UI и обработали его. Но в отличие от других реализаций, он был обработан в Reducer. Здесь он будет отвечать за все — он будет знать, нужно ли ему отправить какую-то команду Актеру, или ему нужно сформировать новое Состояние, или нужно отправить Эффект, или сделать все сразу. Например, Редьюсер может сказать ему изменить состояние с нормального на состояние загрузки и начать отправку запроса Актеру. Как только Актер выполнит свою работу, он отправит Event.Enternal, Редьюсер поймает его и сможет делать все, что ему нужно дальше.

Плюсы:

  1. Низкий порог входа.
  2. Подключается напрямую к представлению — не требуется дополнительная модель представления и т. д.
  3. Масштабируемость, но есть отличие от MVICore или MVIKotlin. Вы можете использовать готовые реализации «пустых» Актера и Редюсера.
  4. Поддержка SingleLiveEvent.
  5. Удобно тестировать Редуктор.
  6. Есть журналирование и отладка путешествий во времени.
  7. Есть генерация кода и плагин для Android Studio.
  8. Есть базовые классы для поддержки Android, но библиотека не привязана к Android.

Минусы:

  1. Нет поддержки Kotlin Multiplatform.
  2. Только RxJava.

Есть и другие библиотеки архитектуры Вяза — например, Эльмо, Чайник, Пуэр.

Заключение

Если посмотреть на примерную картину внешнего вида рассматриваемых в статье архитектур и библиотек, то она выглядит так:

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

Библиотека MVICore отлично себя зарекомендовала. Несмотря на более высокий порог входа, он имеет огромное количество преимуществ, а главное, имеет большое сообщество, которое может сыграть значительную роль в решении нетривиальной задачи.

Библиотека Elmslie отлично подходит как для больших, так и для небольших проектов — с ней гораздо проще разобраться, она более новая и использует несколько иной подход, чем MVI. Лучшая вещь для тех, кто любит экспериментировать. :)

Библиотека Orbit хорошо подходит для небольших проектов, так как имеет чуть меньший функционал. Time Travel Debugging здесь пока не реализован, но это компенсируется другими плюсами. Вполне вероятно, что в будущем единственный минус будет исправлен.

Если вам нужна поддержка Kotlin Multiplatform, MVIKotlin — хороший вариант для крупных проектов, а Orbit — для небольших проектов.

Чтобы вы могли продолжить знакомство с архитектурой MVI, вот несколько дополнительных материалов по теме: