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