
Чтобы проще было развивать и поддерживать код продукта, сложную логику можно разбить на конечное множество состояний и описать правила переходов между ними.
В итоге мы получаем конечный автомат.
Часть бизнес-логики, описывающая смену состояний в MVI-архитектуре, может быть реализована в виде конечного автомата. Это даст возможность представить вашу логику в виде графа переходов для последующей визуализации и анализа.
Мы написали и выложили в опенсорс MVI-библиотеку на Kotlin — VisualFSM, которая умеет по исходному коду строить визуализацию вашей системы, что позволит быстрее понимать сложные бизнес-процессы, упрощать поиск ошибок, добавлять новую функциональность и проводить рефакторинг.
Под катом я расскажу подробнее о нашем подходе, о том, как устроена библиотека, и как начать ее использовать.
MVI и FSM
MVI (Model-View-Intent) — архитектурный паттерн, который следует подходу "однонаправленный поток данных" (unidirectional data flow). Данные передаются от Model к View в одном направлении.
В VisualFSM Intent реализуется в виде действия (Action), в котором описываются возможные переходы состояний (Transition).

FSM (Finite-state machine, конечный автомат) — абстрактная сущность, которая может находиться только в одном из конечного количества состояний в определённый момент. Она может переходить из одного состояния в другое в ответ на входные данные.

Входной алфавит FSM — это объекты действий (Action). Выходным алфавитом являются объекты состояний (State). В каждый момент времени FSM находится в одном из конечного множества состояний (State).
MVI-архитектура хорошо сочетается с абстракцией FSM, в MVI у модели один вход и один выход, так же как в FSM. Если соединить выход View со входом FSM и наоборот то можно объединить две концепции, сделать это удобно позволяет библиотека VisualFSM.

Плюсы VisualFSM
Один набор моделей
Один набор классов Action и State используется для реализации MVI и описания FSM.
Построение по исходному коду
Анализ исходного кода и построение графа выполняется с помощью рефлексии и реализован отдельным модулем, что даёт возможность подключить его только к тестовой среде.
Не требуется написания отдельных конфигураторов для FSM, достаточно добавлять новые классы State и Action – они становятся частью графа состояний и переходов FSM.
Визуализация диаграммы состояний

Визуализация позволяет быстрее понимать сложные бизнес-процессы, упрощает поиск ошибок, помогает добавлять новую функциональность и проводить рефакторинг.
Если нужно добавить или убрать дополнительный диалог или экран, то, смотря на схему, проще понять, какие переходы и состояния нужно изменить.
Кроме того, глядя на схему, можно судить об оптимальности схемы, планировать объединение или наоборот выделять части в отдельные FSM.
В тестировании — полная схема состояний и переходов помогает тестировщику описать сценарии проверки.
Анализ диаграммы состояний
Тестовые инструменты дают возможность выполнять такие распространенные проверки, как проверка на достижимость всех состояний и проверка множества терминальных состояний (для выявления незапланированных тупиковых состояний).
Также можно получить граф в виде списка ребер или словаря смежности для реализации прочих проверок в unit-тестах.
Сопоставив полный граф переходов и граф фактических переходов, выполненных на CI в процессе прохождения функциональных тестов, можно выявить переходы и состояния, не покрытые автотестами. Подробнее о том, как мы выполняем такой анализ, расскажем в отдельной статье. Чтобы не пропустить, подписывайтесь на @visualfsm в Telegram.
Запись фактических переходов во время выполнения Ui тестов можно произвести в файл с помощью реализации TransitionCallbacks интерфейса.
Отсутствие side-effects
В библиотеке нет SingleEvent шины, все transform функции чистые. Благодаря этому нельзя отправить или обработать Event, не привязанный к текущему состоянию.
Подробнее про недостатки SingleEvents можно почитать в статье Android DevRel Manuel Vivo: ViewModel: One-off event antipatterns
Если необходимо отобразить тост, snackbar или диалог, это рекомендуется сделать через изменение состояния (см. Login.snackBarMessage в примере).
Концепция AsyncWorker

AsyncWorker запускает асинхронный запрос или останавливает его, если ему по подписке придёт соответствующий State. Как только запрос завершится успешно или с ошибкой, результат необходимо передать в FSM, вызвав Action, и в FSM будет установлен новый State.
Асинхронная работа может быть представлена отдельными состояниями – благодаря этому мы имеем единый набор состояний, которые выстраиваются в ориентированный граф. Объект AsyncWorker упрощает обработку состояний, в которых выполняется асинхронная работа.
Есть два способа описания состояния, в котором ведется асинхронная работа
1) Отдельное состояние, обозначающее асинхронную работу (например, AsyncWorkState.Loading), каждое такое состояние видно на диаграмме состояний (рекомендуется, если есть цепочка асинхронных состояний)
2) Флаг асинхронной работы внутри конкретного состояния (state.loading)
Как это работает? Базовые классы VisualFSM
State в VisualFSM
State – интерфейс-метка для обозначения классов состояний.
Пример реализации State — AuthFSMState.
Action в VisualFSM
Action — базовый класс действия, является входным объектом для FSM и описывает правила переходов в другие состояния, используя классы Transition. В зависимости от текущего State у FSM и заданного предиката (функции predicate) конструируется State, в который нужно перейти.
Пример реализации Action — actions.
Transition в VisualFSM
Transition — базовый класс перехода, реализуется как inner class в Action. Для
каждого Transition нужно указать два generic параметра <FROM : State, TO : State>:
- FROM —
State, из которого происходит переход. - TO —
State, в котором будет находиться FSM после отработкиtransform.
Классам наследникам Transition необходимо реализовать функцию transform, а при наличии ветвления переопределить функцию predicate.
Функции predicate и transform у Transition
predicateописывает условие выбораTransitionна основе входных данных (переданных в конструкторAction), является одним из условий выбораTransition. Первым условием является совпадение текущего состояния со стартовым дляTransition, указанным в generic. Если нет несколькихTransitionс совпадающим стартовымState,predicateможно не переопределять.transformконструирует новое состояние для выполнения перехода.
AsyncWorker в VisualFSM
AsyncWorker управляет запуском и остановкой асинхронной работы.
Подробнее о конфигурации AsyncWorker и доступных стратегий запуска и остановки операций в документации.
Пример реализации AsyncWorker — AuthFSMAsyncWorker.
Feature в VisualFSM
Feature — фасад к FSM, предоставляет подписку на State и принимает Action для обработки.
@GenerateTransitionsFactory
class AuthFeature(initialState: AuthFSMState) : Feature<AuthFSMState, AuthFSMAction>(
initialState = initialState,
asyncWorker = AuthFSMAsyncWorker(AuthInteractor()), // Используйте DI
transitionsFactory = provideTransitionsFactory()
)
val authFeature = AuthFeature(
initialState = AuthFSMState.Login("", "")
)
// Подписка на состояния в Feature
authFeature.observeState().collect { state -> }
// Подписка на состояния в FeatureRx
authFeature.observeState().subscribe { state -> }
// Выполнение Action
authFeature.proceed(Authenticate("", ""))Пример реализации Feature — AuthFeature.
TransitionCallbacks в VisualFSM
TransitionCallbacks предоставляет функции обратного вызова для сторонней логики. Их удобно использовать для логгирования, записи бизнес метрик или отладки:
fun onActionLaunched(...)—Actionзапускается.fun onTransitionSelected(...)—Transitionвыбран.fun onNewStateReduced(...)—Stateбыл создан.fun onNoTransitionError(...)— нет доступныхTransitionдля перехода.fun onMultipleTransitionError(...)— доступно несколькоTransitionдля перехода.
Инструменты VisualFSM
VisualFSM.generateDigraph(...): String— сгенерировать граф в DOT формате для визуализации в Graphviz, используйте аргументuseTransitionNameдля подстановки имениTransitionилиActionкласса в качестве имени ребра или аннотацию@Edge("name")дляTransitionкласса, чтобы установить произвольное имя ребра.VisualFSM.getUnreachableStates(...): List<KClass<out STATE>>— получить список всех недостижимых состояний от начального состояния.VisualFSM.getFinalStates(...): List<KClass<out STATE>>— получить список всех терминальных состояний.VisualFSM.getEdgeListGraph(...): List<Triple<KClass<out STATE>, KClass<out STATE>, String>>— получить список ребер.VisualFSM.getAdjacencyMap(...): Map<KClass<out STATE>, List<KClass<out STATE>>>— получить словарь смежности.
Пример использования инструментов — AuthFSMTests.
Кодогенерация
Для сокращения шаблонного кода в реализациях Action классов мы используем KSP кодогенерацию. Генерируемым классом является TransitionsFactory для FSM, в котором инициализируются списки переходов для каждого Action.
Подходы в работе с состоянием при использовании VisualFSM
- FSM экрана — этот подход удобен для реализации сложных экранов, изменяющих свое содержимое в зависимости от состояния.
- FSM функционального блока — хорошим примером являются сквозные FSM, работающие независимо от конкретного экрана, но при этом данные состояний необходимо отображать в разных частях приложения.
- Глобальная FSM — все приложение можно описать в виде одной большой FSM. Такой подход можно использовать для небольших приложений, так как в процессе разрастания функциональности рано или поздно захочется выделять отдельные процессы в собственные FSM, иначе диаграмма состояний постепенно становится все менее читаема и между состояниями приходится предавать много данных.
- Комбинации подходов — можно иметь одну глобальную FSM, отвечающую, например, за навигацию в приложении, несколько FSM функциональных блоков и несколько FSM сложных экранов.
Восстановление состояния после пересоздания процесса или Activity
Для возобновления работы FSM с определенного состояния его необходимо передать в конструктор Feature.
Если вы используете DI, то модуль, содержащий объекты FSM, должен инициализироваться после вызова onCreate Activity или Fragment, когда становится доступным savedInstanceState Bundle, в котором было сохранено состояние.
Объект State при этом должен реализовать интерфейс Parcelable и быть передан в Bundle метода onSaveInstanceState.
Пример Koin DI модуля, зависимого от сохраненного состояния — Modules.kt.
Примеры использования
Android приложение (Kotlin Coroutines, Jetpack Compose)
KMM (Android + iOS) приложение (Kotlin Coroutines, Jetpack Compose, SwiftUI)
Как использовать в вашем проекте
Подключение библиотеки в проект описано в Quickstart
Текущее состояние и планы развития
Библиотека используется в проекте Контур.Маркет Касса.
В разработке плагин для IntelliJ IDEA и Android Studio для визуализации диаграммы состояний в IDE и навигации по классам FSM из диаграммы.
Подробнее о проекте, в котором родилась идея библиотеки, и историю о первом неудачном подходе можно посмотреть в записи доклада Mobius Spring 2022: Василий Рылов — MVI и State Machine — визуализация и анализ диаграммы состояний с помощью VisualFSM.
О новых релизах мы рассказываем в Telegram канале.
Обсудить вопрос применения библиотеки или проблему, с которой вы столкнулись, можно в чате поддержки библиотеки.
