
Finite-state machine DSL for explicit state graphs with exhaustive state types, event payloads for constructing states, observable reactive state stream, deterministic non-reentrant dispatch, and Mermaid export.
A finite state machine for Kotlin Multiplatform
KSM is a finite state machine for defining explicit state graphs.
In particular this state machine offers a few nice features:
The state machine itself is:
KClass references, no annotation processing or code generation requiredimplementation("coffee.adammakes.ksm:ksm:<version>")val appLaunchStateMachine = stateMachine<AppStates, AppEvents> {
initialState = AppStates.Uninitialized //Tell the machine what state to start in
dispatchedOn = coroutineScope //Give it a context to dispatch events and run them
//Then create your graph:
//define a "fromState"
state<AppStates.Uninitialized> {
//"on" specifies what event triggers a transition
// and "transitionTo" says what state to go to.
on<AppEvents.EulaOutOfDate>() transitionTo AppStates.RequestEula
//States can be nested sealed classes
on<AppEvents.EulaAccepted>() transitionTo AppStates.Login.CredentialsPrompt
}
state<AppStates.Login.CredentialsPrompt> {
on<AppEvents.LoginSuccess>() transitionTo AppStates.GoToMain
//Events can carry payloads and pass them into new states via transitionWith
on<AppEvents.LoginFailed>() transitionWith { _, event -> AppStates.Login.Failed(event.reason) }
}
}//Just collect the flow
appLaunchStateMachine.currentState.collect { newState -> ... }
//Dispatch events to trigger transitions
appLaunchStateMachine.dispatchEvent(EulaOutOfDate)The state machine itself should not be performing work internally and is intended to function purely as a mapper (CurrentState, Event) -> ResultState.
⚠️ I/O, network calls, persistence should be triggered in response to a state transition, and not inside the state machine itself.
Check out the detailed sample KMP app in the /sample/ directory.
This demonstrates exposing a StateFlow from a ViewModel to @Composable UI. Since states are data classes, they can also be marked @Serializable and stored in Android's SavedStateHandle—so the UI can pick up right where you left off.
Launch the app to jump into a choose your own adventure style dialog flow. Can you defeat the monsters 🧌 and claim the treasure 👑? or will fate have a different plan for you 💀?
All KSM state machines in your project can be exported as Mermaid diagrams at compile time via a Kotlin IR compiler plugin.
Apply the plugin in your build.gradle.kts:
plugins {
id("coffee.adammakes.ksm.ir") version "<version>"
}That's it. Every compileKotlin task will automatically write .mmd files to build/ksmGraphs/ — one per state machine found in your source.
stateDiagram-v2
Uninitialized --> RequestEula: EulaOutOfDate
Uninitialized --> Login.CredentialsPrompt: EulaAccepted
RequestEula --> Login.CredentialsPrompt: EulaAccepted
RequestEula --> ExitApp: EulaDenied
Login.CredentialsPrompt --> GoToMain: LoginSuccess
Login.CredentialsPrompt --> Login.LoginFailed: LoginFailed💡 Tip: If the diagram looks wrong, your code might be wrong. These diagrams are a sanity check and a great way to review state transitions visually.
You can model state transitions with sealed classes and when statements.
Most teams do and it works fine till your codebase begins to grow. You can read my blog for more details about why it's a bad idea. But the short and sweet version is this.
Typical problems with when-based transitions:
KSM solves this by:
(State, Event)
If your logic can be described as a flowchart, KSM keeps it a flowchart.
Copyright (C) 2025 Adam Ward
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0
A finite state machine for Kotlin Multiplatform
KSM is a finite state machine for defining explicit state graphs.
In particular this state machine offers a few nice features:
The state machine itself is:
KClass references, no annotation processing or code generation requiredimplementation("coffee.adammakes.ksm:ksm:<version>")val appLaunchStateMachine = stateMachine<AppStates, AppEvents> {
initialState = AppStates.Uninitialized //Tell the machine what state to start in
dispatchedOn = coroutineScope //Give it a context to dispatch events and run them
//Then create your graph:
//define a "fromState"
state<AppStates.Uninitialized> {
//"on" specifies what event triggers a transition
// and "transitionTo" says what state to go to.
on<AppEvents.EulaOutOfDate>() transitionTo AppStates.RequestEula
//States can be nested sealed classes
on<AppEvents.EulaAccepted>() transitionTo AppStates.Login.CredentialsPrompt
}
state<AppStates.Login.CredentialsPrompt> {
on<AppEvents.LoginSuccess>() transitionTo AppStates.GoToMain
//Events can carry payloads and pass them into new states via transitionWith
on<AppEvents.LoginFailed>() transitionWith { _, event -> AppStates.Login.Failed(event.reason) }
}
}//Just collect the flow
appLaunchStateMachine.currentState.collect { newState -> ... }
//Dispatch events to trigger transitions
appLaunchStateMachine.dispatchEvent(EulaOutOfDate)The state machine itself should not be performing work internally and is intended to function purely as a mapper (CurrentState, Event) -> ResultState.
⚠️ I/O, network calls, persistence should be triggered in response to a state transition, and not inside the state machine itself.
Check out the detailed sample KMP app in the /sample/ directory.
This demonstrates exposing a StateFlow from a ViewModel to @Composable UI. Since states are data classes, they can also be marked @Serializable and stored in Android's SavedStateHandle—so the UI can pick up right where you left off.
Launch the app to jump into a choose your own adventure style dialog flow. Can you defeat the monsters 🧌 and claim the treasure 👑? or will fate have a different plan for you 💀?
All KSM state machines in your project can be exported as Mermaid diagrams at compile time via a Kotlin IR compiler plugin.
Apply the plugin in your build.gradle.kts:
plugins {
id("coffee.adammakes.ksm.ir") version "<version>"
}That's it. Every compileKotlin task will automatically write .mmd files to build/ksmGraphs/ — one per state machine found in your source.
stateDiagram-v2
Uninitialized --> RequestEula: EulaOutOfDate
Uninitialized --> Login.CredentialsPrompt: EulaAccepted
RequestEula --> Login.CredentialsPrompt: EulaAccepted
RequestEula --> ExitApp: EulaDenied
Login.CredentialsPrompt --> GoToMain: LoginSuccess
Login.CredentialsPrompt --> Login.LoginFailed: LoginFailed💡 Tip: If the diagram looks wrong, your code might be wrong. These diagrams are a sanity check and a great way to review state transitions visually.
You can model state transitions with sealed classes and when statements.
Most teams do and it works fine till your codebase begins to grow. You can read my blog for more details about why it's a bad idea. But the short and sweet version is this.
Typical problems with when-based transitions:
KSM solves this by:
(State, Event)
If your logic can be described as a flowchart, KSM keeps it a flowchart.
Copyright (C) 2025 Adam Ward
This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0