
Чтобы проще было развивать и поддерживать код продукта, сложную логику можно разбить на конечное множество состояний и описать правила переходов между ними.
В итоге мы получаем конечный автомат.
Часть бизнес-логики, описывающая смену состояний в 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 упрощает обработку состояний, в которых выполняется асинхронная работа.
Есть два способа описания состояния, в котором ведется асинхронная работа:
- Отдельное состояние, обозначающее асинхронную работу (например, AsyncWorkState.Loading), каждое такое состояние видно на диаграмме состояний (рекомендуется, если есть цепочка асинхронных состояний)
- Флаг асинхронной работы внутри конкретного состояния (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 канале.
Обсудить вопрос применения библиотеки или проблему, с которой вы столкнулись, можно в чате поддержки библиотеки.