- Notifications
You must be signed in to change notification settings - Fork6
VisualFSM - Kotlin Multiplatform library for FSM based MVI with visualization and analysis tools
License
Kontur-Mobile/VisualFSM
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
ENG |RUS
VisualFSM is a Kotlin Multiplatform library for implements anFSM-based (Finite-state machine)[2]MVI pattern(Model-View-Intent)[1] and a set of tools for visualization and analysis ofFSM's diagram of states.
The graph is being built from source code ofFSM's implementation. There is no need of customwritten configurations forFSM, you can just create new State and Action classes, they would beautomatically added to the graph of States and Transitions.
Source code analysis and the graph built are being performed with reflection and declared as aseparate module that would allow it to be connected to testing environment.
Base classes for Android, JVM and KMP projects (Feature and AsyncWorker coroutines edition)
implementation("ru.kontur.mobile.visualfsm:visualfsm-core:$visualfsmVersion")Support of RxJava 3 (FeatureRx, AsyncWorkerRx and dependent classes)
implementation("ru.kontur.mobile.visualfsm:visualfsm-rxjava3:$visualfsmVersion")Code generation
ksp("ru.kontur.mobile.visualfsm:visualfsm-compiler:$visualfsmVersion")Classes for easy getting generated code
implementation("ru.kontur.mobile.visualfsm:visualfsm-providers:$visualfsmVersion")Graph creation and analysis
testImplementation("ru.kontur.mobile.visualfsm:visualfsm-tools:$visualfsmVersion")See inQuickstart
See inExternal state source (optional)
Visualization lets you spend less time on understanding complex business process and makes it easierfordebugging,adding new features, andrefactoring old ones.
A simplified FSM graph sample of user authorization and registration.
To increase the readability of the graph, you can control the rendering rules using the 'DotAttributes' object when generating the graph.You can use 'DotAttributesDefaultPreset' class or create own preset for your project.
Validation on reachability for all states, on set of terminal states and lack of unexpected dead-endstates, custom graph checks in unit tests.
Every async work can be represented by separate states, because of this we can have a common set ofstates that are lining up to a directed graph.
An AsyncWorker allows you to simplify the processing of states with asynchronous work.
The main entities areState,Action,Transition,Feature,AsyncWorker,TransitionCallbacks.
State is an interface to mark State classes.
Action is a base class for action, used as an input object for FSM and describes the transitionrules to other states byTransition classes. A state is being selected depending of the currentFSM'sState and provided predicate (thepredicate function). There are two scenarios that wouldsay the transition rules were set wrong:
- If there are several
Transitions that would fit the specified conditions: aStatethe FSM wasin is inside aTransitionand apredicatereturnstrue— there would be an error passed toaTransitionCallbacks,onMultipleTransitionErrorwould be called, and the firstsuitableTransitionwould be executed. - In case no
Transtionwill do, an error would be passed to aTransitionCallbacks,onNoTransitionErrorwould be called, and aStatewon't be changed.
Transition is a base transition class and is declared as an inner class in anAction. There mustbe two genericStates for everyTransition: aState, the one the transition is going from, andaState that is going to be current for FSM after atransofrm execution.
For the inherited classes ofTransition you need to override atransform method andapredicate method, butpredicate must be overridden only if you have more than oneTransitionwith similar startStates.
predicatedescribes the conditions of aTransition's choice depending on input data that waspassed to anAction's constructor. It is a one of conditions for the choice ofTransition. Thefirst condition is that the currentStatehas to be the same as theTransition's startStatewhich was specified in generic. You might not to overridepredicateif you don't have more thanoneTransitionwith matching startStates.transformcreates a newStatefor aTransition.
Transition is a basic type ofTransition. It can accept the following generic parameters:State or a set ofState as a sealed class
Transitions forming for `Transition`
Let's take a look at the examplesealedclassFSMState :State {data objectInitial :FSMState()sealedclassAsyncWorkerState :FSMState() {data objectLoadingRemote :AsyncWorkerState()data objectLoadingCache :AsyncWorkerState() }data objectLoaded :FSMState()}
Ifdata object Initial anddata object Loaded are passed to the generic parameter
innerclassTransition :Transition<Initial,Loaded>() {overridefuntransform(state:Initial):Loaded {// ... }}
A possibility of the following transitions appears in the FSM:
Initial->Loaded
Ifdata object Initial andsealed class AsyncWorkerState are passed to the generic parameter
innerclassTransition :Transition<Initial,AsyncWorkerState>() {overridefuntransform(state:Initial):AsyncWorkerState {// ... }}
A possibility of the following transitions appears in the FSM:
Initial->AsyncWorkerState.LoadingRemoteInitial->AsyncWorkerState.LoadingCache
Ifsealed class AsyncWorkerState andsealed class AsyncWorkerState are passed to the generic parameter
innerclassTransition :Transition<AsyncWorkerState,AsyncWorkerState>() {overridefuntransform(state:AsyncWorkerState):AsyncWorkerState {// ... }}
A possibility of the following transitions appears in the FSM:
AsyncWorkerState.LoadingRemote->AsyncWorkerState.LoadingRemoteAsyncWorkerState.LoadingRemote->AsyncWorkerState.LoadingCacheAsyncWorkerState.LoadingCache->AsyncWorkerState.LoadingCacheAsyncWorkerState.LoadingCache->AsyncWorkerState.LoadingRemote
Ifsealed class AsyncWorkerState anddata object Loaded are passed to the generic parameter
innerclassTransition :Transition<AsyncWorkerState,Loaded>() {overridefuntransform(state:AsyncWorkerState):Loaded {// ... }}
A possibility of the following transitions appears in the FSM:
AsyncWorkerState.LoadingRemote->LoadedAsyncWorkerState.LoadingCache->Loaded
SelfTransition is a type ofTransition that implements a transition fromState toState with the same type. It can accept the following generic parameters:State or a set ofState as a sealed class
Transitions forming for `SelfTransition`
Let's take a look at the examplesealedclassFSMState :State {data objectInitial :FSMState()sealedclassAsyncWorkerState :FSMState() {data objectLoadingRemote :AsyncWorkerState()data objectLoadingCache :AsyncWorkerState() }data objectLoaded :FSMState()}
Ifdata object Initial is passed to the generic parameter
innerclassTransition :SelfTransition<Initial>() {overridefuntransform(state:Initial):Initial {// ... }}
A possibility of the following transitions appears in the FSM:
Initial->Initial
Ifsealed class AsyncWorkerState is passed to the generic parameter
innerclassTransition :SelfTransition<AsyncWorkerState>() {overridefuntransform(state:AsyncWorkerState):AsyncWorkerState {// ... }}
A possibility of the following transitions appears in the FSM:
AsyncWorkerState.LoadingRemote->AsyncWorkerState.LoadingRemoteAsyncWorkerState.LoadingCache->AsyncWorkerState.LoadingCache
AsyncWorker controls the start and stop of async tasks.AsyncWorker starts async requests orstops them it it gets specifiedState via a subscription. As long as the request completes witheither success or error, theAction will be called and the FSM will be set with a newState. Forconvenience those states that are responsible for async tasks launch, it is recommended to join theminAsyncWorkState.
To subscribe toState, you need to override theonNextState method, and for each state to constructAsyncWorkerTask for processing in the AsyncWorker.For each operation result (success and error) you must call the proceed method and passAction to handle the result.Don't forget to handle each task's errors inonNextState method, if an unhandled exception occurs,then fsm may stuck in the current state and the onStateSubscriptionError method will be called.
There might be a case when we can get aState via a subscription that is fully equivalent tocurrent running async request, so for this case there are two type of AsyncWorkTask:
- AsyncWorkerTask.ExecuteIfNotExist - launch only if operation with equals state is not currently running (priority isgiven to a running operation with equals state object)
- AsyncWorkerTask.ExecuteIfNotExistWithSameClass - launch only if operation with same state class is not currently running (priority isgiven to a running operation with same state class, used for tasks that deliver the result in several stages)
- AsyncWorkerTask.ExecuteAndCancelExist - relaunch async work (priority is for the new on).
To handle a state change to state without async work, you must use a task:
- AsyncWorkerTask.Cancel - stop asynchronous work, if running
Feature is the facade for FSM, provides subscription on currentState, andproceeds incomingActions.
TransitionCallbacks gives access to method callbacks for third party logic. They are great forlogging,debugging,metrics, etc. on six available events: when initialState is received, whenAction is launched,whenTransition is selected, a newState had been reduced, and two error events —noTransitions or multipleTransitions available.
Logging parameters for the built-in logger, if the capabilities of the built-in logger are not enough,useTransitionCallbacks to implement your own logging andLoggerMode.NONE in theLogParams arguments.
VisualFSM.generateDigraph(...): String- generate a FSM DOT graph for visualization in Graphviz (graphviz cli on CI orhttp://www.webgraphviz.com/ in browser).Transitionclass name used as the edge name, you can use the@Edge("name")annotation on theTransitionclass to set a custom edge name.For customization entire graph, colors and shapes of nodes or edges you can use theattributesargument to graph rendering customization.VisualFSM.getUnreachableStates(...): List<KClass<out STATE>>- get all unreachable states from initial stateVisualFSM.getFinalStates(...): List<KClass<out STATE>>- get all final statesVisualFSM.getEdgeListGraph(...): List<Triple<KClass<out STATE>, KClass<out STATE>, String>>- builds an Edge ListVisualFSM.getAdjacencyMap(...): Map<KClass<out STATE>, List<KClass<out STATE>>>- builds an Adjacency Map of states
To analyze FSM using third-party tools, it is possible to generate a csv file with all transitions.To generate a file, you need to pass thegenerateAllTransitionsCsvFiles parameter with the valuetrue to the ksp parameters.
ksp { arg("generateAllTransitionsCsvFiles","true")}In the package that contains theFeature, a file called[Base State Name]AllTransitions.csv will be generated with lines in the manner:
[Name of the transition],[Name of the State from which the transition executes],[Name of the State to which the transition executes]A tests sample for FSM of user authorization andregistration:AuthFSMTests.kt
The DOT visualization graph for graphviz is being generated using theVisualFSM.generateDigraph(...) method.
For CI visualization usegraphviz, for the local visualization (on yourPC) useedotor,webgraphviz, or other DOT graph visualization tool.
// Use Feature with Kotlin Coroutines or FeatureRx with RxJava@GenerateTransitionsFactory// Use this annotation for generation TransitionsFactoryclassAuthFeature(initialState:AuthFSMState) : Feature<AuthFSMState, AuthFSMAction>( initialState = initialState, asyncWorker =AuthFSMAsyncWorker(AuthInteractor()), transitionCallbacks =TransitionCallbacksImpl(),// Tip - use DI transitionsFactory = provideTransitionsFactory(),// Get an instance of the generated TransitionsFactory// Getting an instance of a generated TransitionsFactory for KMP projects:// Name generated by mask Generated[FeatureName]TransitionsFactory()// transitionsFactory = GeneratedAuthFeatureTransitionsFactory(), // Until the first start of code generation, the class will not be visible in the IDE.)val authFeature=AuthFeature( initialState=AuthFSMState.Login("",""))// Observe states on FeatureauthFeature.observeState().collect { state-> }// Observe states on FeatureRxauthFeature.observeState().subscribe { state-> }// Proceed ActionauthFeature.proceed(Authenticate("",""))
AllStates are listed in a sealed class. For the convenienceStates that call async work isrecommended to group inside innerAsyncWorkState sealed class.
sealedclassAuthFSMState :State {data classLogin(valmail:String,valpassword:String,valerrorMessage:String? =null ) : AuthFSMState()data classRegistration(valmail:String,valpassword:String,valrepeatedPassword:String,valerrorMessage:String? =null ) : AuthFSMState()data classConfirmationRequested(valmail:String,valpassword:String ) : AuthFSMState()sealedclassAsyncWorkState :AuthFSMState() {data classAuthenticating(valmail:String,valpassword:String ) : AsyncWorkState()data classRegistering(valmail:String,valpassword:String ) : AsyncWorkState() }data classUserAuthorized(valmail:String) : AuthFSMState()}
AsyncWorker subscribes on state changes, starts async tasks for those inAsyncWorkState group, andcallsAction to process the result after the async work is done.
classAuthFSMAsyncWorker(privatevalauthInteractor:AuthInteractor) : AsyncWorker<AuthFSMState, AuthFSMAction>() {overridefunonNextState(state:AuthFSMState):AsyncWorkerTask<AuthFSMState> {returnwhen (state) {isAsyncWorkState.Authenticating-> {AsyncWorkerTask.ExecuteAndCancelExist(state) {val result= authInteractor.check(state.mail, state.password) proceed(HandleAuthResult(result)) } }isAsyncWorkState.Registering-> {AsyncWorkerTask.ExecuteIfNotExist(state) {val result= authInteractor.register(state.mail, state.password) proceed(HandleRegistrationResult(result)) } }else->AsyncWorkerTask.Cancel() } }}
HandleRegistrationResult is one ofActions of the sample authorization and registration FSM thatis called fromAsyncWorker after the result of registration is received. It consists oftwoTransitions, the necessaryTransition is chosen afterpredicate function result.
classHandleRegistrationResult(valresult:RegistrationResult) : AuthFSMAction() {innerclassSuccess :Transition<AsyncWorkState.Registering,Login>() {overridefunpredicate(state:AsyncWorkState.Registering)= result==RegistrationResult.SUCCESSoverridefuntransform(state:AsyncWorkState.Registering):Login {returnLogin(state.mail, state.password) } }innerclassBadCredential :Transition<AsyncWorkState.Registering,Registration>() {overridefunpredicate(state:AsyncWorkState.Registering)= result==RegistrationResult.BAD_CREDENTIALoverridefuntransform(state:AsyncWorkState.Registering):Registration {returnRegistration(state.mail, state.password,"Bad credential") } }innerclassConnectionFailed :Transition<AsyncWorkState.Registering,Registration>() {overridefunpredicate(state:AsyncWorkState.Registering)= result==RegistrationResult.NO_INTERNEToverridefuntransform(state:AsyncWorkState.Registering):Registration {returnRegistration(state.mail, state.password, state.password,"No internet") } }}
classAuthFSMTests { @TestfungenerateDigraph() {println(VisualFSM.generateDigraph( baseAction=AuthFSMAction::class, baseState=AuthFSMState::class, initialState=AuthFSMState.Login::class, ) )Assertions.assertTrue(true) } @TestfunallStatesReachableTest() {val notReachableStates=VisualFSM.getUnreachableStates( baseAction=AuthFSMAction::class, baseState=AuthFSMState::class, initialState=AuthFSMState.Login::class, )Assertions.assertTrue( notReachableStates.isEmpty(),"FSM have unreachable states:${notReachableStates.joinToString(",")}" ) } @TestfunoneFinalStateTest() {val finalStates=VisualFSM.getFinalStates( baseAction=AuthFSMAction::class, baseState=AuthFSMState::class, )Assertions.assertTrue( finalStates.size==1&& finalStates.contains(AuthFSMState.UserAuthorized::class),"FSM have not correct final states:${finalStates.joinToString(",")}" ) }}
Success,AsyncWorkState.Registering,LoginBadCredential,AsyncWorkState.Registering,RegistrationConnectionFailed,AsyncWorkState.Registering,RegistrationMVI stands forModel-View-Intent. It is an architectural pattern that utilizesunidirectionaldata flow. The data circulates betweenModel andView only in one direction - fromModeltoView and fromView toModel.
Afinite-state machine (FSM) is an abstract machine that can be in exactly one of a finite numberof states at any given time. TheFSM can change from one state to another in response to someinputs.
About
VisualFSM - Kotlin Multiplatform library for FSM based MVI with visualization and analysis tools
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Uh oh!
There was an error while loading.Please reload this page.
Contributors5
Uh oh!
There was an error while loading.Please reload this page.



