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