Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/psh/kotlin-state-machine
A multiplatform state machine with clean Kotlin DSL
https://github.com/psh/kotlin-state-machine
event-driven finite-state-machine fsm kmm kotlin multiplatform multiplatform-kotlin-library state-machine
Last synced: 2 months ago
JSON representation
A multiplatform state machine with clean Kotlin DSL
- Host: GitHub
- URL: https://github.com/psh/kotlin-state-machine
- Owner: psh
- License: apache-2.0
- Created: 2020-09-19T02:43:06.000Z (over 4 years ago)
- Default Branch: main
- Last Pushed: 2024-04-16T02:30:10.000Z (8 months ago)
- Last Synced: 2024-10-09T17:56:06.414Z (2 months ago)
- Topics: event-driven, finite-state-machine, fsm, kmm, kotlin, multiplatform, multiplatform-kotlin-library, state-machine
- Language: Kotlin
- Homepage:
- Size: 556 KB
- Stars: 16
- Watchers: 2
- Forks: 3
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
- License: LICENSE
- Code of conduct: CODE_OF_CONDUCT.md
Awesome Lists containing this project
README
![GitHub top language](https://img.shields.io/github/languages/top/psh/kotlin-state-machine)
[![ktlint](https://img.shields.io/badge/Kotlin%20Multiplatform-%E2%9D%A4-FF4081)](https://kotlinlang.org/docs/multiplatform.html)
[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
[![Build Status](https://app.travis-ci.com/psh/kotlin-state-machine.svg?branch=main)](https://app.travis-ci.com/github/psh/kotlin-state-machine)
[![Current Version](https://img.shields.io/badge/Version-0.5.1-1abc9c.svg)](https://shields.io/)
[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)# kotlin-state-machine
Some pointers to the layout of this project -
* Code lives in `commonMain` as the state machine itself is a multiplatform library.
* Tests live in `commonTest` and should always pass for ALL platforms.
* Examples use the library published to your `mavenLocal` repo to best simulate end-user usage scenarios. Run the `publishToMavenLocal` gradle task before running the examples and all should be fine.## Roadmap
* ![Version 0.6](https://img.shields.io/badge/version-0.6-green) ![In Progress](https://img.shields.io/badge/-In%20Progress-orange)
* **Overhaul of the documentation to reflect current state**
* **Compose integrations**
* ![Version 0.5](https://img.shields.io/badge/version-0.5-green)
* Full DSL / API review to ensure that it makes sense
* Work through the callback and coroutines API to make sure that it also makes sense
* ![Version 0.4](https://img.shields.io/badge/version-0.4-green)
* Move the tests into `commonTest` so they can be run across all platforms
* Fix reported bugs (thanks Jigar for reporting the issue, and steps to reproduce it)
* ![Version 0.3](https://img.shields.io/badge/version-0.3-green)
* Execute state transitions as coroutines
* ![Version 0.2](https://img.shields.io/badge/version-0.2-green)
* Embraced Kotlin multiplatform
* Use the Gradle version catalog to simplify the build
* NOTE: this library is built against the **NEW** native memory module introduced in Kotlin 1.6.10
* ![Version 0.1](https://img.shields.io/badge/version-0.1-green )
* introduced the state machine and its declarative DSL for defining states and transitions
* built as a traditional JVM library.(later) Publish the library to Maven Central
Somewhere along the way, there need to be additional examples written, for native, Android and iOS.
## Example State Machine
![Matter State Diagram](examples/matter/state-diagram.png)
Our state machine has three states:
```kotlin
sealed class MatterState : State {
object Solid : MatterState()
object Liquid : MatterState()
object Gas : MatterState()
}
```Allowing us to define a simple state machine for matter:
```kotlin
val stateMachine = graph {
initialState(Solid)state(Solid) {
allows(Liquid)
}state(Liquid) {
allows(Solid, Gas)
}state(Gas) {
allows(Liquid)
}
}
```Which can then be driven by calling `transitionTo()`:
```kotlin
stateMachine.start()// sublimation not allowed - stays in Solid
stateMachine.transitionTo(Gas)// melt the solid
stateMachine.transitionTo(Liquid)// vaporize the liquid
stateMachine.transitionTo(Gas)
```Note: if you prefer, you can have multiple `allows()` definitions rather than the comma-separated list
```kotlin
state(Liquid) {
allows(Solid)
allows(Gas)
}
```## Event Driven State Machine
Transitions between states can be triggered by events:
```kotlin
sealed class MatterEvent : Event {
object OnMelted : MatterEvent()
object OnFrozen : MatterEvent()
object OnVaporized : MatterEvent()
object OnCondensed : MatterEvent()
}
```Allowing us to define an event-driven state machine that focuses more on the _edges_ between the nodes (the red arrows
in the state diagram):```kotlin
val stateMachine = graph {
initialState(Solid)state(Solid) {
on(OnMelted) {
transitionTo(Liquid)
}
}state(Liquid) {
on(OnFrozen) {
transitionTo(Solid)
}
on(OnVaporized) {
transitionTo(Gas)
}
}state(Gas) {
on(OnCondensed) {
transitionTo(Liquid)
}
}
}
```Note: defining the state transition using `transitionTo()` implicitly sets up the list of allowed transitions for a
given state; there is no need to use `allows()` when using `transitionTo()`.This event-driven state machine can then be driven by calling `consume()` or, `transitionTo()`
```kotlin
stateMachine.start()// sublimation not allowed - stays in Solid
stateMachine.consume(OnVaporized)// melt the solid
stateMachine.consume(OnMelted)// vaporize the liquid
stateMachine.transitionTo(Gas)
```## Code Execution Triggers
The simplest execution triggers are _entry_ and _exit_ of our states:
```kotlin
state(Solid) {
onEnter {
// code executed each time we enter the Solid state
}onExit {
// code executed each time we leave the Solid state
}on(OnMelted) { transitionTo(Liquid) }
}
```However, the state machine also has the concept of _edges_ between the nodes of the graph. It's possible to execute code
as we
_enter_ and _exit_ the transition (that is, at the start and the end of the red lines in the state diagram):```kotlin
// Event driven style
state(Solid) {
onEnter { }
onExit { }on(OnMelted) {
onEnter {
// code executed each time we enter the
// transition state from Solid --> Liquid
}onExit {
// code executed each time we exit the
// transition state from Solid --> Liquid
}transitionTo(Liquid)
}
}// Non-event driven
state(Liquid) {
onEnter { }
onExit { }
onTransitionTo(Gas) {
onEnter { }
onExit { }
}
}
```In this scenario, consuming the `OnMelted` event will trigger a transition which will execute the following steps:
1. `Node` Solid OnExit
2. `Edge` Solid --> Liquid OnEnter
3. `Edge` Solid --> Liquid OnExit
4. `Node` Liquid OnEnter## Decision States
![Even-Odd Diagram](examples/even-odd/state-diagram.png)
If you include a `decision` in a state definition, it will be executed in preference to the normal `onEnter`. The return
value from the decision lambda will be processed as if `consume()` had been called, with all the normal event handling /
transition rules. A return value `null` or other unhandled event wont cause a transition.```kotlin
graph {
initialState(StateA)state(StateA) { allows(StateB) }
state(StateB) {
decision { /* returns an event, or null */ }
on(TestEvent) { transitionTo(StateA) }
on(OtherTestEvent) { transitionTo(StateC) }
}state(StateC)
}
```## Observing State Changes
Suppose you have a basic state machine
```kotlin
val stateMachine = graph {
initialState(Solid)state(Solid) { ... }
state(Liquid) { ... }
state(Gas) { ... }
}
```The graph you build is *observable* - you can observe either the states themselves as things change over time, or the lower-level state transitions (that includes edge traversal)
```kotlin
stateMachine.observeState().collect { state ->
// called with each state that we land in eg, Solid or Gas
}stateMachine.observeStateChanges().collect { machineState ->
// called when dwelling on a particular node,
// eg, MachineState.Dwelling( Gas )
//
// or when traversing an edge of the graph,
// eg, MachineState.Traversing( Liquid to Gas )
}```
## Starting The Machine At A Given State
State machines are defined to be in an _inactive_ state when they are first defined (the large black dot on the state
diagram). A call to `start()` is required to make the initial transition into the defined _initialState_. Optionally a _machine state_
can be passed into the `start()` method to start the state machine at an arbitrary node in the graph. The state machine
allows either `Inactive` or `Dwelling` machine states to start and will throw an exception if you try to start
with `Traversing`.```kotlin
@Test
fun `freezing should move us from liquid to solid`() {
// Given
stateMachine.start(Dwelling(Liquid))// When
stateMachine.consume(OnFrozen)// Then
assertEquals(Solid, stateMachine.currentState.id)
}
```The `start()` method can be called at any time (and even multiple times) to reset the state machine to a given node in
the graph.## Conditional & Long Running Transitions
**NOTE** This section of the library is in-flux and subject to deep changes
By default, state transitions are instantaneous and never fail. You can supply a block of code that will override that
behavior, allowing for long-running operations that have the option to succeed or fail. The assumption is that the
action succeeds, so you only need to notify the state machine if there is a failure:```kotlin
state(Solid) {
on(OnMelted) {
onEnter { }
onExit { }
transitionTo(Liquid)
execute { result ->
/* Do something that might take a while */if ( /* something went wrong */ ) {
failure()
}
}
}
}
```and for a non-event driven state machines :
```kotlin
state(Solid) {
onTransitionTo(Liquid) {
onEnter { }
onExit { }
execute { result ->
/* Do something that might take a while */if ( /* some condition */ ) {
result.success()
} else {
result.failure()
}
}
}
}
```By default, when a transition fails, the _exit_ block of the edge will not be called and the state machine will re-enter
the "from" state of the transition.1. `Node` Solid OnExit
2. `Edge` Solid --> Liquid OnEnter
3. `Node` Solid OnEnterAn application might be tempted to show and hide a progress indicator (`onEnter` / `onExit`) while making a REST service
call (using `execute`)
but the lack of a call to the `onExit` when a transition fails would leave the progress indicator visible. In that case
the call to `failure()`
can be replaced with `failAndExit()` to ensure that the `onEnter` / `onExit` are still executed as a pair.The execution block can be combined with the call to `transitionTo()` for a more concise syntax
```kotlin
state(Solid) {
on(OnMelted) {
transitionTo(Liquid) { result ->
/* Do something that might take a while */if ( /* some condition */ ) {
result.success()
} else {
result.failure()
}
}
}
}
```Note: Be aware that the state machine _will be left in limbo_ (the transition will never complete) if none of the
success or failure methods are called.