This work is licensed under Apache License 2.0
kfsm is a small Kotlin library for finite state machines. It has a Kotlin DSL.
In kfsm, an event can cause a transition from one state to another state. A transition can run an action. It can also run an entry action and an exit action.
-
Event-driven state machine.
-
External transitions and internal transitions.
-
Entry actions and exit actions for each state.
-
Default actions for each state.
-
Default entry actions and default exit actions.
-
A query for the allowed events.
-
Multiple state maps with push transitions and pop transitions.
-
Automatic transitions.
-
Externalized state.
-
Typed event arguments and return values.
-
✓ Multiple state maps
-
✓ Push transitions and pop transitions
-
✓ Automatic transitions
-
✓ Externalized state
-
✓ Typed event arguments
-
✓ Typed event return values
-
✓ Simple visualization
-
✓ Detailed visualization
-
✓ Gradle plugin for visualization
-
✓ Timeouts
-
✓ Coroutines
-
❏ A different context type for nested state maps
This is the classic turnstile state machine from SMC.
Assume that you must manage the state of a simple lock.
The lock() function must run only when the lock is not locked.
The unlock() function must run only when the lock is locked.
Use the DSL to declare a state machine definition that matches the diagram:
| Start State | Event | End State | Action |
|---|---|---|---|
LOCKED |
PASS |
LOCKED |
alarm |
LOCKED |
COIN |
UNLOCKED |
unlock |
UNLOCKED |
PASS |
LOCKED |
lock |
UNLOCKED |
COIN |
UNLOCKED |
returnCoin |
class Turnstile(var locked: Boolean = true) {
fun unlock() {
assert(locked) { "Cannot unlock when not locked" }
println("Unlock")
locked = false
}
fun lock() {
assert(!locked) { "Cannot lock when locked" }
println("Lock")
locked = true
}
fun alarm() {
println("Alarm")
}
fun returnCoin() {
println("Return coin")
}
override fun toString(): String {
return "Turnstile(locked=$locked)"
}
}Declare two enums. One enum has the states. The other enum has the events.
enum class TurnstileStates {
LOCKED,
UNLOCKED
}
enum class TurnstileEvents {
COIN,
PASS
}class TurnstileFSM(turnstile: Turnstile) {
private val fsm = definition.create(turnstile)
fun coin() = fsm.sendEvent(TurnstileEvents.COIN)
fun pass() = fsm.sendEvent(TurnstileEvents.PASS)
companion object {
private val definition = stateMachine(
TurnstileStates.values().toSet(),
TurnstileEvents::class,
Turnstile::class
) {
initialState {
if (locked)
TurnstileStates.LOCKED
else
TurnstileStates.UNLOCKED
}
default {
onEntry { startState, targetState, _ ->
println("entering:$startState -> $targetState for $this")
}
// default transition will invoke alarm
action { state, event, _ ->
println("Default action for state($state) -> event($event) for $this")
alarm()
}
onExit { startState, targetState, _ ->
println("exiting:$startState -> $targetState for $this")
}
}
// when current state is LOCKED
whenState(TurnstileStates.LOCKED) {
// external transition on COIN to UNLOCKED state
onEvent(TurnstileEvents.COIN to TurnstileStates.UNLOCKED) {
unlock()
}
}
// when current state is UNLOCKED
whenState(TurnstileStates.UNLOCKED) {
// internal transition on COIN
onEvent(TurnstileEvents.COIN) {
returnCoin()
}
// external transition on PASS to LOCKED state
onEvent(TurnstileEvents.PASS to TurnstileStates.LOCKED) {
lock()
}
}
}.build()
}
}This definition says:
-
When the state is
LOCKEDand the event isCOIN, change toUNLOCKEDand run the lambda{ unlock() }. The lambda runs as a member of the context. -
When the state is
LOCKEDand the event isPASS, run the default actionalarm(). The state staysLOCKED.
Create an instance of the state machine. Give it the context that the actions operate on:
val turnstile = Turnstile()
val fsm = TurnstileFSM(turnstile)The context is independent of the state machine.
When you send an event, the state machine can run actions:
// The state is LOCKED
fsm.coin()
// Expect the unlock() action. The new state is UNLOCKED
fsm.pass()
// Expect the lock() action. The new state is LOCKED
fsm.pass()
// Expect the alarm() action. The state stays LOCKED
fsm.coin()
// Expect the unlock() action. The new state is UNLOCKED
fsm.coin()
// Expect the returnCoin() action. The state stays UNLOCKEDYou can create an instance of the state machine when you need it, if the context has values that represent the state. The context must keep its own state correctly.
The state machine can derive the state from the values of the properties of the context.
The Documentation has more detail about how to define state machines.
The documentation has examples for:
Releases are on Maven Central. Snapshots are in the GitHub Packages Maven repository. Contact us for a personal access token (PAT) to get snapshot releases.
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("org.infosmid:kfsm:2.0.0")
}
}
}repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation 'org.infosmid:kfsm:2.0.0'
}
}
}
}repositories {
mavenCentral()
}
dependencies {
implementation("org.infosmid:kfsm:2.0.0")
}Declare the library in the version catalog:
[versions]
kfsm = "2.0.0"
[libraries]
infosmid-kfsm = { module = "org.infosmid:kfsm", version.ref = "kfsm" }Then use the catalog accessor in the build script.
For Kotlin Multiplatform
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain {
dependencies {
implementation(libs.infosmid.kfsm)
}
}
}
}For a single platform (JVM / Android)
For more information about the visualization options, see kfsm-viz.




