
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 required(State, Event) -> State
implementation("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)Side effects are async work triggered upon state entry: network calls, timers, analytics, db writes - things that affect the outside world. However their result feeds back as an event. To do so I include an effects module:
implementation("coffee.adammakes.ksm:ksm-effects:<version>")Wrap your state machine with withEffects and register per-state work using the onEnter DSL:
val effectedMachine = appLaunchStateMachine.withEffects(coroutineScope) {
onEnter<AppStates.Login.CredentialsPrompt>() effect ::attemptAutoLogin
onEnter<AppStates.GoToMain>() effect ::loadUserProfile
}Each effect is a suspend (State) -> Event. Only one effect can be registered per state — registering a second one for the same state throws. When the machine leaves a state, an in-flight effect for that state is cancelled automatically. When an effect completes, the returned event is dispatched back into the machine.
Effects registered via withEffects also appear as notes in generated Mermaid diagrams.
Transition reducers should stay pure and are intended to function as a mapper
(CurrentState, Event) -> ResultState.
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>"
}
//optionally set an output directory
ksm { outputDir = layout.projectDirectory.dir("ksmGraphs") }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 required(State, Event) -> State
implementation("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)Side effects are async work triggered upon state entry: network calls, timers, analytics, db writes - things that affect the outside world. However their result feeds back as an event. To do so I include an effects module:
implementation("coffee.adammakes.ksm:ksm-effects:<version>")Wrap your state machine with withEffects and register per-state work using the onEnter DSL:
val effectedMachine = appLaunchStateMachine.withEffects(coroutineScope) {
onEnter<AppStates.Login.CredentialsPrompt>() effect ::attemptAutoLogin
onEnter<AppStates.GoToMain>() effect ::loadUserProfile
}Each effect is a suspend (State) -> Event. Only one effect can be registered per state — registering a second one for the same state throws. When the machine leaves a state, an in-flight effect for that state is cancelled automatically. When an effect completes, the returned event is dispatched back into the machine.
Effects registered via withEffects also appear as notes in generated Mermaid diagrams.
Transition reducers should stay pure and are intended to function as a mapper
(CurrentState, Event) -> ResultState.
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>"
}
//optionally set an output directory
ksm { outputDir = layout.projectDirectory.dir("ksmGraphs") }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