August 3, 2022

Опыт перехода на mvi 

Эта статья из Telegram канала «Разработка Юлы». Хотите больше полезных материалов и новостей разработки под Android и iOS, описания схем реализации, ссылки на библиотеки и приглашения на митапы команды - подпишитесь на канал.

Что такое MVI?

MVI (Model-View-Intent) – это архитектурный паттерн, Unidirectional Data Flow подход проектирования системы, в которой все представляется в виде однонаправленного потока действий и управления состояния.

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

Мы создаем интерфейс-маркер, который будет отвечать за какое-либо событие, состояние экрана и при этом не влиять на состояние экрана.

При таком подходе состояние представления (View) описывается моделью (Model), которая сохранена в объекте ViewModel.

View подписывается на изменения модели – в нашем случае путем RxJava– и делает изменения в себе в соответствии с новым состоянием.

View может взаимодействовать с ViewModel путем простого вызова методов, или, как у нас было реализовано, путем реактивной подписки ViewModel на новые события, которые отправляет View.

Как и почему мы реализовали свой MVI-фреймворк на Android?

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

С какими проблемами мы столкнулись в результате?

 1. код на больших и сложных экранах становился трудным в поддержке и расширении;

2. из-за ошибок проектирования мы сталкивались с неконсистентностью системы,

3. отсутствие логирования приводило к долгому поиску багов.

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

MVI строится на основе трех компонентов – модели, намерения (действия) и состояния экрана. Логика приложения диктуется пользователем: например, он хочет загрузить картинку в высоком разрешении, и различными внешними эффектами (далее – side-effects), например, внезапной потерей соединения.

MVI подход решают задачу реализовать механизм, который будет обрабатывать намерения пользователя и их следствия (например, ответ от api).

Наш опыт внедрения MVI-фреймворка.

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

  • Масштабируемость и независимость от платформы и внешних библиотек. Архитектура должна быть в крайней степени гибкой и расширяемой.
  • Сейчас у нас в проекте используется RxJava, при этом мы планируем перейти на compose и использовать coroutines в будущем.
    Отсюда требование: фреймворк не должен зависеть на сторонние библиотеки.
  • Сопровождаемость.
    Чем проще исправлять ошибки и управлять  проектом после передачи в эксплуатацию, тем легче новым разработчикам поддерживать проект;
  • Надежность.
    Внутри реализации системы исключены проблемы многопоточной среды. Единый контракт должен обеспечивать безопасность интерфейсов;
  • Тестируемость;
  • Возможность переиспользования;
  • Легкая встраиваемость в проект;
  • Детальные и хорошо читаемые логи.

Дополнительные критерий – активное комьюнити, поскольку библиотека должна сохранять  актуальность, ее должны обновлять, ревьюить и поддерживать в порядке.

В момент, когда мы изучали opensource решения, и не нашли нужного, помог наш активный круг общения и дружная команда :)

Собственное решение реализовал наш бывший коллега Георгий Ипполитов – reduktor. Мы совместно посмотрели предложенный Гошей вариант, после чего он внес  значительные доработки и довел либу до идеального состояния, отвечающего всем нашим критериям.

Библиотека прошла 2 волны улучшений: от первой версии мы довели ее до второй и окончательной, изменив основу либы (Store). В результате мы получили:

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

А вот и вики либы:

https://github.com/g000sha256/reduktor/tree/wiki

Всё взаимодействие происходит через объект Store. У него есть одно публичное поле – states. При подписке на эту цепочку сразу будет отдано актуальное состояние в текущем потоке. Однако дальнейшие обновления могут приходить в других потоках. Внутри есть проверка состояний на эквивалентность.

Принцип работы:

  • Действие может попасть в систему через Initializer. Это может быть событие от взаимодействия с пользователем или любое другое внешнее событие (например, новое сообщение из сокета).
  • Далее действие проходит через Reducer. Тут действие может повлиять на изменение состояния. Если состояние изменилось,  информация об этом будет отправлена тем, кто на него подписался.
  • После этого действие и новое (либо не изменившееся) состояние попадают в SideEffect.
  • Оттуда возвращаются новые цепочки с действиями. Подписываемся на них и ожидаем событий.
  • Новые действия будут снова отправлены в Reducer.