MVI и State Machine — объединяем концепции. Визуализация и анализ диаграммы состояний в Android и KMM проектах

pt6sdtqbnvruzc4ojawm14xsew8.png

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

В итоге мы получаем конечный автомат.

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

Мы написали и выложили в опенсорс MVI-библиотеку на Kotlin — VisualFSM, которая умеет по исходному коду строить визуализацию вашей системы, что позволит быстрее понимать сложные бизнес-процессы, упрощать поиск ошибок, добавлять новую функциональность и проводить рефакторинг.

Под катом я расскажу подробнее о нашем подходе, о том, как устроена библиотека, и как начать ее использовать.


MVI и FSM

MVI (Model-View-Intent) — архитектурный паттерн, который следует подходу «однонаправленный поток данных» (unidirectional data flow). Данные передаются от Model к View в одном направлении.
В VisualFSM Intent реализуется в виде действия (Action), в котором описываются возможные переходы состояний (Transition).

qyljkkqzr4ki712zedgdtsl6a0q.png

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

pkgjjwumz1gna6yotlnia-xvgke.png

Входной алфавит FSM — это объекты действий (Action). Выходным алфавитом являются объекты состояний (State). В каждый момент времени FSM находится в одном из конечного множества состояний (State).

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

amrc1bm7urzc296frzv4krskleo.png


Плюсы VisualFSM


Один набор моделей

Один набор классов Action и State используется для реализации MVI и описания FSM.


Построение по исходному коду

Анализ исходного кода и построение графа выполняется с помощью рефлексии и реализован отдельным модулем, что даёт возможность подключить его только к тестовой среде.

Не требуется написания отдельных конфигураторов для FSM, достаточно добавлять новые классы State и Action — они становятся частью графа состояний и переходов FSM.


Визуализация диаграммы состояний

axnpg3qfid9lrli1z8huturu4k4.png

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

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

Кроме того, глядя на схему, можно судить об оптимальности схемы, планировать объединение или наоборот выделять части в отдельные 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

urgch7ungpg81bel5tzcmk4mvee.png

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, в котором будет находиться 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(
    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> — получить список всех недостижимых состояний от начального состояния.
  • VisualFSM.getFinalStates(...): List> — получить список всех терминальных состояний.
  • VisualFSM.getEdgeListGraph(...): List, KClass, String>> — получить список ребер.
  • VisualFSM.getAdjacencyMap(...): Map, List>> — получить словарь смежности.

Пример использования инструментов — 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 канале.

Обсудить вопрос применения библиотеки или проблему, с которой вы столкнулись, можно в чате поддержки библиотеки.

© Habrahabr.ru