{"id":15568407,"url":"https://github.com/psh/kotlin-state-machine","last_synced_at":"2025-04-24T00:05:59.182Z","repository":{"id":46479765,"uuid":"296772553","full_name":"psh/kotlin-state-machine","owner":"psh","description":"A multiplatform state machine with clean Kotlin DSL","archived":false,"fork":false,"pushed_at":"2024-04-16T02:30:10.000Z","size":569,"stargazers_count":17,"open_issues_count":5,"forks_count":3,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-24T00:05:52.286Z","etag":null,"topics":["event-driven","finite-state-machine","fsm","kmm","kotlin","multiplatform","multiplatform-kotlin-library","state-machine"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/psh.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-09-19T02:43:06.000Z","updated_at":"2024-12-05T07:33:12.000Z","dependencies_parsed_at":"2025-03-06T22:41:41.636Z","dependency_job_id":null,"html_url":"https://github.com/psh/kotlin-state-machine","commit_stats":null,"previous_names":[],"tags_count":2,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psh%2Fkotlin-state-machine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psh%2Fkotlin-state-machine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psh%2Fkotlin-state-machine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/psh%2Fkotlin-state-machine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/psh","download_url":"https://codeload.github.com/psh/kotlin-state-machine/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":250535098,"owners_count":21446508,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["event-driven","finite-state-machine","fsm","kmm","kotlin","multiplatform","multiplatform-kotlin-library","state-machine"],"created_at":"2024-10-02T17:15:18.348Z","updated_at":"2025-04-24T00:05:59.155Z","avatar_url":"https://github.com/psh.png","language":"Kotlin","readme":"![GitHub top language](https://img.shields.io/github/languages/top/psh/kotlin-state-machine)\n[![ktlint](https://img.shields.io/badge/Kotlin%20Multiplatform-%E2%9D%A4-FF4081)](https://kotlinlang.org/docs/multiplatform.html)\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![Build Status](https://app.travis-ci.com/psh/kotlin-state-machine.svg?branch=main)](https://app.travis-ci.com/github/psh/kotlin-state-machine)\n[![Current Version](https://img.shields.io/badge/Version-0.5.1-1abc9c.svg)](https://shields.io/)\n[![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/)\n\n# kotlin-state-machine\n\nSome pointers to the layout of this project - \n* Code lives in `commonMain` as the state machine itself is a multiplatform library.\n* Tests live in `commonTest` and should always pass for ALL platforms. \n* 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. \n\n## Roadmap\n* ![Version 0.6](https://img.shields.io/badge/version-0.6-green) ![In Progress](https://img.shields.io/badge/-In%20Progress-orange)\n  * **Overhaul of the documentation to reflect current state**\n  * **Compose integrations**\n* ![Version 0.5](https://img.shields.io/badge/version-0.5-green)\n  * Full DSL / API review to ensure that it makes sense\n  * Work through the callback and coroutines API to make sure that it also makes sense\n* ![Version 0.4](https://img.shields.io/badge/version-0.4-green)\n  * Move the tests into `commonTest` so they can be run across all platforms\n  * Fix reported bugs (thanks Jigar for reporting the issue, and steps to reproduce it)\n* ![Version 0.3](https://img.shields.io/badge/version-0.3-green)\n  * Execute state transitions as coroutines\n* ![Version 0.2](https://img.shields.io/badge/version-0.2-green)\n    * Embraced Kotlin multiplatform  \n    * Use the Gradle version catalog to simplify the build\n    * NOTE: this library is built against the **NEW** native memory module introduced in Kotlin 1.6.10\n* ![Version 0.1](https://img.shields.io/badge/version-0.1-green )\n  * introduced the state machine and its declarative DSL for defining states and transitions\n  * built as a traditional JVM library. \n\n(later) Publish the library to Maven Central \n\nSomewhere along the way, there need to be additional examples written, for native, Android and iOS.\n\n## Example State Machine\n\n![Matter State Diagram](examples/matter/state-diagram.png)\n\nOur state machine has three states:\n\n```kotlin\nsealed class MatterState : State {\n    object Solid : MatterState()\n    object Liquid : MatterState()\n    object Gas : MatterState()\n}\n```\n\nAllowing us to define a simple state machine for matter:\n\n```kotlin\nval stateMachine = graph {\n    initialState(Solid)\n\n    state(Solid) {\n        allows(Liquid)\n    }\n\n    state(Liquid) {\n        allows(Solid, Gas)\n    }\n\n    state(Gas) {\n        allows(Liquid)\n    }\n}\n```\n\nWhich can then be driven by calling `transitionTo()`:\n\n```kotlin\nstateMachine.start()\n\n// sublimation not allowed - stays in Solid\nstateMachine.transitionTo(Gas)\n\n// melt the solid\nstateMachine.transitionTo(Liquid)\n\n// vaporize the liquid\nstateMachine.transitionTo(Gas)\n```\n\nNote: if you prefer, you can have multiple `allows()` definitions rather than the comma-separated list\n\n```kotlin\nstate(Liquid) {\n    allows(Solid)\n    allows(Gas)\n}\n```\n\n## Event Driven State Machine\n\nTransitions between states can be triggered by events:\n\n```kotlin\nsealed class MatterEvent : Event {\n    object OnMelted : MatterEvent()\n    object OnFrozen : MatterEvent()\n    object OnVaporized : MatterEvent()\n    object OnCondensed : MatterEvent()\n}\n```\n\nAllowing us to define an event-driven state machine that focuses more on the _edges_ between the nodes (the red arrows\nin the state diagram):\n\n```kotlin\nval stateMachine = graph {\n    initialState(Solid)\n\n    state(Solid) {\n        on(OnMelted) {\n            transitionTo(Liquid)\n        }\n    }\n\n    state(Liquid) {\n        on(OnFrozen) {\n            transitionTo(Solid)\n        }\n        on(OnVaporized) {\n            transitionTo(Gas)\n        }\n    }\n\n    state(Gas) {\n        on(OnCondensed) {\n            transitionTo(Liquid)\n        }\n    }\n}\n```\n\nNote: defining the state transition using `transitionTo()` implicitly sets up the list of allowed transitions for a\ngiven state; there is no need to use `allows()` when using `transitionTo()`.\n\nThis event-driven state machine can then be driven by calling `consume()` or, `transitionTo()`\n\n```kotlin\nstateMachine.start()\n\n// sublimation not allowed - stays in Solid\nstateMachine.consume(OnVaporized)\n\n// melt the solid\nstateMachine.consume(OnMelted)\n\n// vaporize the liquid\nstateMachine.transitionTo(Gas)\n```\n\n## Code Execution Triggers\n\nThe simplest execution triggers are _entry_ and _exit_ of our states:\n\n```kotlin\nstate(Solid) {\n    onEnter {\n        // code executed each time we enter the Solid state \n    }\n\n    onExit {\n        // code executed each time we leave the Solid state \n    }\n\n    on(OnMelted) { transitionTo(Liquid) }\n}\n```\n\nHowever, the state machine also has the concept of _edges_ between the nodes of the graph. It's possible to execute code\nas we\n_enter_ and _exit_ the transition (that is, at the start and the end of the red lines in the state diagram):\n\n```kotlin\n// Event driven style\nstate(Solid) {\n    onEnter { }\n    onExit { }\n\n    on(OnMelted) {\n        onEnter {\n            // code executed each time we enter the \n            // transition state from Solid --\u003e Liquid \n        }\n\n        onExit {\n            // code executed each time we exit the \n            // transition state from Solid --\u003e Liquid \n        }\n\n        transitionTo(Liquid)\n    }\n}\n\n// Non-event driven\nstate(Liquid) {\n    onEnter { }\n    onExit { }\n    onTransitionTo(Gas) {\n        onEnter { }\n        onExit { }\n    }\n}\n```\n\nIn this scenario, consuming the `OnMelted` event will trigger a transition which will execute the following steps:\n\n1. `Node` Solid OnExit\n2. `Edge` Solid --\u003e Liquid OnEnter\n3. `Edge` Solid --\u003e Liquid OnExit\n4. `Node` Liquid OnEnter\n\n## Decision States\n\n![Even-Odd Diagram](examples/even-odd/state-diagram.png)\n\nIf you include a `decision` in a state definition, it will be executed in preference to the normal `onEnter`. The return\nvalue from the decision lambda will be processed as if `consume()` had been called, with all the normal event handling /\ntransition rules. A return value `null` or other unhandled event wont cause a transition.\n\n```kotlin\ngraph {\n    initialState(StateA)\n\n    state(StateA) { allows(StateB) }\n\n    state(StateB) {\n        decision { /* returns an event, or null */ }\n        on(TestEvent) { transitionTo(StateA) }\n        on(OtherTestEvent) { transitionTo(StateC) }\n    }\n\n    state(StateC)\n}\n```\n\n## Observing State Changes\n\nSuppose you have a basic state machine\n```kotlin\nval stateMachine = graph {\n    initialState(Solid)\n\n    state(Solid) { ... }\n    state(Liquid) { ... }\n    state(Gas) { ... }\n}\n```\n\nThe 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)\n\n```kotlin\nstateMachine.observeState().collect { state -\u003e\n    // called with each state that we land in eg, Solid or Gas\n}\n\nstateMachine.observeStateChanges().collect { machineState -\u003e\n    // called when dwelling on a particular node, \n    // eg, MachineState.Dwelling( Gas )\n    //\n    // or when traversing an edge of the graph,\n    // eg, MachineState.Traversing( Liquid to Gas )\n}\n\n```\n\n## Starting The Machine At A Given State\n\nState machines are defined to be in an _inactive_ state when they are first defined (the large black dot on the state\ndiagram). A call to `start()` is required to make the initial transition into the defined _initialState_. Optionally a _machine state_\ncan be passed into the `start()` method to start the state machine at an arbitrary node in the graph. The state machine\nallows either `Inactive` or `Dwelling` machine states to start and will throw an exception if you try to start\nwith `Traversing`.\n\n```kotlin\n@Test\nfun `freezing should move us from liquid to solid`() {\n    // Given\n    stateMachine.start(Dwelling(Liquid))\n\n    // When\n    stateMachine.consume(OnFrozen)\n\n    // Then\n    assertEquals(Solid, stateMachine.currentState.id)\n}\n```\n\nThe `start()` method can be called at any time (and even multiple times) to reset the state machine to a given node in\nthe graph.\n\n## Conditional \u0026 Long Running Transitions\n\n**NOTE** This section of the library is in-flux and subject to deep changes\n\nBy default, state transitions are instantaneous and never fail. You can supply a block of code that will override that\nbehavior, allowing for long-running operations that have the option to succeed or fail.  The assumption is that the\naction succeeds, so you only need to notify the state machine if there is a failure:\n\n```kotlin\nstate(Solid) {\n    on(OnMelted) {\n        onEnter { }\n        onExit { }\n        transitionTo(Liquid)\n        execute { result -\u003e\n            /* Do something that might take a while */\n\n            if ( /* something went wrong */ ) {\n                failure()\n            }\n        }\n    }\n}\n``` \n\nand for a non-event driven state machines :\n\n```kotlin\nstate(Solid) {\n    onTransitionTo(Liquid) {\n        onEnter { }\n        onExit { }\n        execute { result -\u003e\n            /* Do something that might take a while */\n\n            if ( /* some condition */ ) {\n                result.success()\n            } else {\n                result.failure()\n            }\n        }\n    }\n}\n``` \n\nBy default, when a transition fails, the _exit_ block of the edge will not be called and the state machine will re-enter\nthe \"from\" state of the transition.\n\n1. `Node` Solid OnExit\n2. `Edge` Solid --\u003e Liquid OnEnter\n3. `Node` Solid OnEnter\n\nAn application might be tempted to show and hide a progress indicator (`onEnter` / `onExit`) while making a REST service\ncall (using `execute`)\nbut the lack of a call to the `onExit` when a transition fails would leave the progress indicator visible. In that case\nthe call to `failure()`\ncan be replaced with `failAndExit()` to ensure that the `onEnter` / `onExit` are still executed as a pair.\n\nThe execution block can be combined with the call to `transitionTo()` for a more concise syntax\n\n```kotlin\nstate(Solid) {\n    on(OnMelted) {\n        transitionTo(Liquid) { result -\u003e\n            /* Do something that might take a while */\n\n            if ( /* some condition */ ) {\n                result.success()\n            } else {\n                result.failure()\n            }\n        }\n    }\n}\n``` \n\nNote: Be aware that the state machine _will be left in limbo_ (the transition will never complete) if none of the\nsuccess or failure methods are called.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsh%2Fkotlin-state-machine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpsh%2Fkotlin-state-machine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpsh%2Fkotlin-state-machine/lists"}