Giter Site home page Giter Site logo

kstatemachine / kstatemachine Goto Github PK

View Code? Open in Web Editor NEW
319.0 6.0 19.0 1.83 MB

KStateMachine is a Kotlin DSL library for creating state machines and statecharts.

Home Page: https://kstatemachine.github.io/kstatemachine/

License: Boost Software License 1.0

Kotlin 100.00%
kotlin state-machine fsm-library fsm state-management transitions hsm nested-states plantuml open-source

kstatemachine's Introduction

KStateMachine

KStateMachine

Build and test with Gradle Quality Gate Status codecov Maven Central Version JitPack multiplatform support

Open Collective JetBrains support Mentioned in Awesome Kotlin Android Arsenal Share on X Share on Reddit

Documentation | Sponsors | Quick start | Samples | Install | Contribution | License | Discussions

KStateMachine is a Kotlin DSL library for creating state machines and statecharts.

Overview

Integration features are:

  • Kotlin DSL syntax. Declarative and clear state machine structure. Using without DSL is also possible.
  • Kotlin Coroutines support. Call suspendable functions within the library. You can fully use KStateMachine without Kotlin Coroutines dependency if necessary.
  • Kotlin Multiplatform support.
  • Zero dependency. It is written in pure Kotlin, it does not depend on any third party libraries or Android SDK.

State management features:

Important

SEE FULL DOCUMENTATION HERE

Sponsors ❤

I highly appreciate that you donate or become a sponsor to support the project. Use ❤️ github-sponsors button to see supported methods and push the ⭐ star-button if you like this project.

Quick start sample

Finishing traffic light

stateDiagram-v2
    direction LR
    [*] --> GreenState
    GreenState --> YellowState: SwitchEvent
    YellowState --> RedState: SwitchEvent
    RedState --> [*]

object SwitchEvent : Event

sealed class States : DefaultState() {
    object GreenState : States()
    object YellowState : States()
    object RedState : States(), FinalState // Machine finishes when enters final state
}

fun main() = runBlocking {
    // Create state machine and configure its states in a setup block
    val machine = createStateMachine(this) {
        addInitialState(GreenState) {
            // Add state listeners
            onEntry { println("Enter green") }
            onExit { println("Exit green") }

            // Setup transition
            transition<SwitchEvent> {
                targetState = YellowState
                // Add transition listener
                onTriggered { println("Transition triggered") }
            }
        }

        addState(YellowState) {
            transition<SwitchEvent>(targetState = RedState)
        }

        addFinalState(RedState)

        onFinished { println("Finished") }
    }

    // Now we can process events
    machine.processEvent(SwitchEvent)
    machine.processEvent(SwitchEvent)
}

Samples

Install

KStateMachine is available on Maven Central and JitPack repositories.

See install section in the docs for details.

Maven Central

dependencies {
    // multiplatform artifacts, where <Tag> is a library version.
    implementation("io.github.nsk90:kstatemachine:<Tag>")
    implementation("io.github.nsk90:kstatemachine-coroutines:<Tag>")
}

Build

Run ./gradlew build or build with Intellij IDEA.

Contribution

The library is in a active development phase. You are welcome to propose useful features and contribute to the project. See CONTRIBUTING file.

Thanks to supporters

Stargazers repo roster for @kstatemachine/kstatemachine Forkers repo roster for @kstatemachine/kstatemachine

License

Licensed under permissive Boost Software License

kstatemachine's People

Contributors

ayanyev avatar jmichaud avatar neugartf avatar nsk90 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

kstatemachine's Issues

Missing listener which reports state changes in an "atomic" way

I have the following sample state machine:

  val machine = createStateMachine(name = "app") {
    initialState("init")
    state("main", childMode = ChildMode.PARALLEL) {
      state("main_screen") {
        initialState("idle")
      }
      state("bottom_bar") {
        initialState("bar_visible")
        state("bar_hidden")
      }
      state("bottom_sheet") {
        initialState("sheet_hidden")
        state("search")
        state("routing")
        state("bookmarks")
        state("more")
      }
    }

and the following listener:

  machine.addListener(object : StateMachine.Listener {
    override fun onStateChanged(newState: IState) {
      println("active states:\n${machine.activeStates().joinToString(",")}")
    }
  })

There are few problems with it. Before I list them I want to say that I think they are caused by the fact that this listener should simply have a different name, something like onStateEntered, because a "state change" usually means the machine arriving at the new state (or set of states) and is usually called after full transition has happened.

From the current name I expected it to behave as "active state change listener" and had the following problems:

  1. When entering the main state this listener reports 3 separate state changes:
    • change to main_screen.idle
    • change to bottom_bar.bar_visible
    • change to bottom_sheet.sheet.hidden
      But the parallel state change is one change, i.e machine goes from init state to all 3 parallel states as one transaction. I mean internally it may do several onEntry in order, but to the user this should be reported as a single state change (it just so happens that the next state consists of 3 sub-states, but it's still one state);
  2. Also when used in non-parallel states, but with child states, this listener reports changes to "compound states", but it should only report "leaf" states. That is if some event changes state from parent1.childA to parent1.parent2.parent3.childB, listener will be called with arguments parent2, parent3, childB while the actual "finalized" state changed only once, because single event moves machine from one state to another (=1 state change) and this listener should be called only once with argument childB. At least this is how all libs that I've used behave...
  3. It has only one argument which doesn't "feel" right in presence of parallel state feature, I'd expect active states set to be passed there...

Perhaps one solution to this is to rename the existing listener somehow, but I'd also like to be able to use the actual state change listener implemented sometime.

Cannot execute multiple test cases

Hi,

Firstly I love this library - it's super useful and clean.

My problem is when I run an automated Junit test suite against a class that hosts a state machine, only the first test executed passes. All subsequent tests fail with the following error:

Multiple transitions match com.example.fsmtesting.Do$Something@21d03963, [(DefaultTransition, ru.nsk.kstatemachine.TargetState@1f760b47), (DefaultTransition, ru.nsk.kstatemachine.TargetState@18ece7f4)] in One
java.lang.IllegalStateException: Multiple transitions match com.example.fsmtesting.Do$Something@21d03963, [(DefaultTransition, ru.nsk.kstatemachine.TargetState@1f760b47), (DefaultTransition, ru.nsk.kstatemachine.TargetState@18ece7f4)] in One
	at ru.nsk.kstatemachine.InternalStateKt.findUniqueResolvedTransition(InternalState.kt:48)
	at ru.nsk.kstatemachine.BaseStateImpl

If I run each test on their own, they will pass, individually.

Can you help? It's driving me crazy!

Matt

exportToPlantUml throws "JobCancellationException: ScopeCoroutine has completed normally;"

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun testUML() = runTest {
        val test = makeUML() // throws "JobCancellationException: ScopeCoroutine has completed normally;"
        assert(test != "")
    }

    suspend fun makeUML() = coroutineScope {
        val m = makeM()
        val uml = m.exportToPlantUml()
        return@coroutineScope uml
    }

    suspend fun makeM() = coroutineScope {
        return@coroutineScope createStateMachine(this) {
            setInitialState(state {  })
        }
    }

The above JUnit5 test testUML will throw JobCancellationException for some reason.
It wouldn't if I put val uml = m.exportToPlantUml() inside makeM though.
I'm corrently using the legacy non-coroutine method as the workaround

Entry/Exit callback not called in Child state

I find cases when Entry/Exit callback not called in Child state.

I try it in latest version 0.9.0
Below is unit test how to reproduce it

package ru.nsk.kstatemachine

import io.kotest.core.spec.style.StringSpec

class AdvancedCrossLevelTransitionTest : StringSpec({

    "1. child to neighbors 1. child and then back 1. child" {
        val callbacks = mockkCallbacks()

        lateinit var state1: State
        lateinit var state11: State
        lateinit var state12: State
        lateinit var state2: State
        lateinit var state21: State
        lateinit var state22: State

        val machine = createStateMachine {
            state1 = initialState("1") {
                callbacks.listen(this)

                state11 = initialState("11") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state12 }
                        callbacks.listen(this)
                    }
                }

                state12 = state("12") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state11 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state2 }
                    callbacks.listen(this)
                }
            }
            state2 = state("2") {
                callbacks.listen(this)

                state21 = initialState("21") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state22 }
                        callbacks.listen(this)
                    }
                }

                state22 = state("22") {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state21 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state1 }
                    callbacks.listen(this)
                }
            }
        }

        //* -> 1 (11)   - ok
        verifySequenceAndClear(callbacks) {
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state12)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 2 (22)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state21)
            callbacks.onEntryState(state22)
        }

        //2 (22) -> 1 (11)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state22)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 1 (11)  - failed (missing child entry callback)
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state21)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)//Missing entry state!
        }

        //1 (11) -> 1 (12)  - failed (missing child exit callback)
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)//Missing exit state!
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - failed (missing child entry callback)
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)//Missing entry state!
        }
    }
})

Solution how to fix it (with very ugly fix in State)

package ru.nsk.kstatemachine

import io.kotest.core.spec.style.StringSpec
import timber.log.Timber
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.full.memberProperties
import kotlin.reflect.jvm.isAccessible

open class FixedState(
        name: String? = null,
        childMode: ChildMode = ChildMode.EXCLUSIVE
) : DefaultState(name, childMode) {

    override fun onDoExit(transitionParams: TransitionParams<*>) {
        super.onDoExit(transitionParams)
        //Need clear CurrentState after exit
        // - not working - reEnter parent state after leaving it with child state (as initial state)
        try {
            DefaultStateWithDetail.propertyCurrentState?.set(this, null)
        } catch (e : Exception) {
            Timber.e(e, "Cannot set value for currentState property!")
        }
    }

    companion object {
        internal var propertyCurrentState : KMutableProperty1<InternalState, Any?>? = null

        init {
            try {
                propertyCurrentState = BaseStateImpl::class.memberProperties
                        .find { it.name == "currentState" }
                        ?.apply { isAccessible = true } as KMutableProperty1<InternalState, Any?>
            } catch (e : Exception){
                Timber.e(e, "Cannot get currentState property!")
            }
        }
    }
}

class FixedAdvancedCrossLevelTransitionTest : StringSpec({
    "1. child to neighbors 1. child and then back 1. child" {
        val callbacks = mockkCallbacks()

        val state1 = FixedState("1")
        val state11 = FixedState("11")
        val state12 = FixedState("12")
        val state2 = FixedState("2")
        val state21 = FixedState("21")
        val state22 = FixedState("22")

        val machine = createStateMachine {
            addInitialState(state1) {
                callbacks.listen(this)

                addInitialState(state11) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state12 }
                        callbacks.listen(this)
                    }
                }

                addState(state12) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state11 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state2 }
                    callbacks.listen(this)
                }
            }
            addState(state2) {
                callbacks.listen(this)

                addInitialState(state21) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state22 }
                        callbacks.listen(this)
                    }
                }

                addState(state22) {
                    callbacks.listen(this)

                    transitionOn<SwitchEvent> {
                        targetState = { state21 }
                        callbacks.listen(this)
                    }
                }

                transitionOn<SwitchEventL1> {
                    targetState = { state1 }
                    callbacks.listen(this)
                }
            }
        }

        //* -> 1 (11)   - ok
        verifySequenceAndClear(callbacks) {
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state12)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 2 (22)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state21)
            callbacks.onEntryState(state22)
        }

        //2 (22) -> 1 (11)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state22)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }

        //2 (21) -> 1 (11)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state21)
            callbacks.onExitState(state2)
            callbacks.onEntryState(state1)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 1 (12)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state11)
            callbacks.onEntryState(state12)
        }

        //1 (12) -> 1 (11)  - ok
        machine.processEvent(SwitchEvent)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEvent)
            callbacks.onExitState(state12)
            callbacks.onEntryState(state11)
        }

        //1 (11) -> 2 (21)  - ok
        machine.processEvent(SwitchEventL1)
        verifySequenceAndClear(callbacks) {
            callbacks.onTriggeredTransition(SwitchEventL1)
            callbacks.onExitState(state11)
            callbacks.onExitState(state1)
            callbacks.onEntryState(state2)
            callbacks.onEntryState(state21)
        }
    }
})

Outdated error message

I tried to create a state machine without enabling undo, and then attempted to undo. I got the following (very helpful!) error message:
"... createStateMachine(isUndoEnabled = true) argument to enable it ..."

However the actual parameter is enableUndo not isUndoEnabled. This message should be updated to reflect the variable name.

[CRITICAL] onFinished is notified BEFORE final onStateChange

    fun StateMachine.stateFlow() = callbackFlow {
        val stateListener = object : StateMachine.Listener {
            override fun onStateChanged(newState: IState) {
                trySendBlocking(newState)
            }
        }
        addListener(stateListener)
        val finishListener = object : IState.Listener {
            override fun onFinished(transitionParams: TransitionParams<*>) {
                channel.close()
            }
        }
        addListener(finishListener)
        awaitClose {
            removeListener(stateListener)
            removeListener(finishListener)
        }
    }

Following code does not pass finalState notification.
It is critcal bug, because it is not possible to create any reliable flow of states, rxjava observable, etc..

[Question] Transition in `onFinished`?

Is it possible to create a transition in onFinished?

My main goal is to define a sets of nested states in different gradle modules and then combine them into a "big" state machine in another gradle module which also defines a transitions between those nested children when they finish.

// this function is defined in an independent gradle module,
// it has no knowldege of "parent", containing states, so it can't mention them
// in `transition` block of `done` state
fun IState.addStates() {
  lateinit var done: State

  initialState("a") {
    transition<Event> {
      targetState = done
    }
  }

  done = finalState("done")
}

// this function is defined in main(root) gradle module, which
// depends on the above module
fun main() {
  val machine = createStateMachine {
    lateinit var s2: State

    initialState("s1") {
      addStates()
      onFinished {
       // this is currently not possible, onFinished is a simple listener
        targetState = s2
      }
    }

    s2 = state("s2") {
      // ...
    }
  }
}

IIRC, something like this is possible in scxml and xstate.js.
Is this possible in kstatemachine? Am I missing some other idiom which can be used to achieve this.

Cound I apply an transition for all states at the same time ?

Hi , sir
it is a great job ,
when I try to apply it into project , I need to add a transition for all states , whatever the src state , I have tried several times ,but still could not add such transition .

for example :
A -> C -> D -> E -> F
and when a special event received, all the state need to jump to G 

I have  read the code in test case , but no inspiration on this point yet
Is there such a way that could add such transition now ?

thanls for your

Listen to group of states or transitions

Sometimes it might be useful to listen to group of states. For example you have 3 states. When your are in 1 you set some flag. Then you go to 2 and then to 3. In 3 flag is dismissed (successful result). But what if state 2 have transitions to another states. We might want to dismiss flag if state machine goes in wrong direction. To solve this problem group of states, nested states or transition path might be useful.

Transitions

        addInitialState(GreenState) {
            // Add state listeners
            onEntry { println("Enter green") }
            onExit { println("Exit green") }

            // Setup transition
            transition<NextEvent> {
                targetState = YellowState
                // Add transition listener
                onTriggered { println("Transition triggered") }
            }
        }

In this example is the onTriggered done before the state is moved to Yellow?

What happens if the onTriggered fails/crashes/throws an exception?

@nsk90

Lack of possibility to clear up state machine

Thanks for nice implementation of state machine.

I have created some state machines which reuses some common states (objects in sealed class).
I run only machine at a time, but after some user option I need to recreate whole state machine in different way.
I do not see any way to clear up state machine, before reconfigure.

class ClearStatesVisitor : Visitor {
    override fun visit(machine: StateMachine) {
        visit(machine as IState)
    }

    override fun visit(state: IState) {
        state.states.forEach(::visit)
        (state.transitions as MutableSet).clear()
        (state.states as MutableSet).clear()
    }

    override fun <E : Event> visit(transition: Transition<E>) {}

  }
}

Something like that is working for me. I know it is incomplete, maybe you can extend it and add some facade function in StateMachine interface (like clearup() or something).

Allow calling processEvents() from listener callbacks

Currently such pending events are passed to PendingEventHandler and there are three options:

  • to throw exception in this case (default)
  • ignore such events
  • store them in user managed queue (user is responsible for calling processEvent for them when current processing completes)

Such queue may be implemented on library side allowing processing of pending events by default.

Add support for multiple transition targets when using parallel states

If I'm not mistaken, this feature is currently missing from kstatemachine.

I'll use existing statechart implementations as an example (again).

  1. In xstate.js having this state machine:
{
  id: 'file',
  type: 'parallel',
  states: {
    upload: {
      initial: 'idle',
      states: {
        idle: {
          on: {
            INIT_UPLOAD: { target: 'pending' }
          }
        },
        pending: {
          on: {
            // NOTE multiple targets here
            UPLOAD_COMPLETE: { target: { upload: 'idle', download: 'pending' } }
          }
        },
        success: {}
      }
    },
    download: {
      initial: 'idle',
      states: {
        idle: {
          on: {
            INIT_DOWNLOAD: { target: 'pending' }
          }
        },
        pending: {
          on: {
            DOWNLOAD_COMPLETE: { target: 'success' }
          }
        },
        success: {}
      }
    }
  }
}

(sample transition above which has multiple targets might have a slight syntax or target specification errors, but it should work something like that, I tried before :))

  1. In SCXML:
<scxml>
  <parallel id="ButtonActivity">
    <state id="Button">
      <state id="BtnOff">
        <!-- NOTE here multiple targets states are specified separated by space -->
        <transition cond="_event.data == 1" event="click" target="Button.BtnOn StateShape2"/>
      </state>
      <state id="BtnOn">
      </state>
    </state>
    <state id="StateShape1"/>
    <state id="StateShape2">
      <transition event="on.released" target="StateShape3"/>
    </state>
    <state id="StateShape3">
      <transition event="on.released" target="StateShape4"/>
    </state>
  </parallel>
</scxml>

It would be very nice to have something like this in kstatemachine.

Wrong condition in transition is hard to maintain

State may contain many transitions. Condition lambda helps to choose correct one. But is easy to make a mistake and have multiple transitions for current event, which is wrong.

Make a State to choose next state may be a better solution. As it should be simpler to make a decision where to go in one place, rather than in may separated lambdas.

Java 8 compatibility

I run into this issue in version 0.15.0 upon compilation.
0.14.0 works fine.

Any workaround to use 0.15.0 ?

> Error while evaluating property 'filteredArgumentsMap' of task ':backend:compileKotlin'
   > Could not resolve all files for configuration ':backend:compileClasspath'.
      > Could not resolve io.github.nsk90:kstatemachine:0.15.0.
        Required by:
            project :backend
         > No matching variant of io.github.nsk90:kstatemachine:0.15.0 was found. The consumer was configured to find an API of a library compatible with Java 8, preferably in the form of class files, preferably optimized for standard JVMs, and its dependencies declared externally, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm' but:
             - Variant 'apiElements' capability io.github.nsk90:kstatemachine:0.15.0 declares an API of a library, packaged as a jar, preferably optimized for standard JVMs, and its dependencies declared externally, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm':
                 - Incompatible because this component declares a component compatible with Java 11 and the consumer needed a component compatible with Java 8
             - Variant 'javadocElements' capability io.github.nsk90:kstatemachine:0.15.0 declares a runtime of a component, and its dependencies declared externally:
                 - Incompatible because this component declares documentation and the consumer needed a library
                 - Other compatible attributes:
                     - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
                     - Doesn't say anything about its target Java version (required compatibility with Java 8)
                     - Doesn't say anything about its elements (required them preferably in the form of class files)
                     - Doesn't say anything about org.jetbrains.kotlin.platform.type (required 'jvm')
             - Variant 'runtimeElements' capability io.github.nsk90:kstatemachine:0.15.0 declares a runtime of a library, packaged as a jar, preferably optimized for standard JVMs, and its dependencies declared externally, as well as attribute 'org.jetbrains.kotlin.platform.type' with value 'jvm':
                 - Incompatible because this component declares a component compatible with Java 11 and the consumer needed a component compatible with Java 8
             - Variant 'sourcesElements' capability io.github.nsk90:kstatemachine:0.15.0 declares a runtime of a component, and its dependencies declared externally:
                 - Incompatible because this component declares documentation and the consumer needed a library
                 - Other compatible attributes:
                     - Doesn't say anything about its target Java environment (preferred optimized for standard JVMs)
                     - Doesn't say anything about its target Java version (required compatibility with Java 8)
                     - Doesn't say anything about its elements (required them preferably in the form of class files)
                     - Doesn't say anything about org.jetbrains.kotlin.platform.type (required 'jvm')
                     ```

Question: How should download progress be handled?

If I have a state data class called data class Downloading(val progress: Int) : DefaultState() how can I update the progress so that when I listen to state changes in the the view, I can update it accordingly?

Make states nested

With nested states it is possible to have a tree of states where child states inherit parent transitions.
It is useful when a group of states have same transitions.

Pass data in FinishedEvent from final DataState.

When we have sub-states and they have a finalDataState, when it is reached, I want to be somehow able to extract the data from FinishedEvent and depending on this data move to some other state in parent.

Currently FinishedEvent is does not have a data field. In xstate.js IIRC all events have "payload" field. In KStateMachine this is explicit: Eventand DataEvent. Maybe FinishedEvent can be data event? Or would this be not the right solution... I'm not sure, wanted to describe the usecase, maybe you'll be able to think of a nice solution.

Here is the sample of what I have tried to do:

data class Ev(override val data: Int) : DataEvent<Int>

fun main() {
  val m = createStateMachine {
    val s2 = state("s2")
    val s3 = state("s3")

    initialState("s1") {
      val childFinal = finalDataState<Int>("child_final")

      initialState("child_first") {
        dataTransition<Ev, Int> {
          targetState = childFinal
        }
      }

      transitionOn<FinishedEvent> {
        targetState = {
          // val data = event.data <-- cannot be done, FinishedEvent doesn't have 'data'

          // Then for some reason I thought that maybe event.state will be `childFinal`
          // and I can extract data from there.
          // this compiles, but crashes at runtime, 
          // because event.state is "s1" actually (and this is correct)
          val data = (event.state as DataState<Int>).data
          if (data == 3) s2 else s3
        }
      }
    }
  }

  m.processEvent(Ev(3)) // expecting to go to s2
  // OR
  m.processEvent(Ev(5)) // expecting to go to s3
}

Originally posted by @dimsuz in #54 (comment)

Support older versions of Kotlin

Hi, my project is running on Kotlin 1.4 and I wish I could make use of this project. Do you think it's possible to be backwards compatible with older versions of Kotlin?

I could work on a PR if the idea is accepted.

Using gradle to generate the plantUml graph

It would be nice to have a way to generate the graph from gradle w/o putting it in the app's code -- for example on Android to be able to retrieve a graph, I'd have to export it on the device, and then actually pull it from there. Whereas with gradle it could be be a git hook to generate the graph before each commit to a master branch -- or part of a CI pipeline with one of the result artifacts being such graphs.

Initial argument for start

Could you provide an optional argument for start method in StateMachine interface (in the similar way as it in processEvent method)? Optional argument should be passed to initialState in state machine.

fun start(argument: Any? = null)

I can live without it, but it will would be nice to have such parameter to avoid workaround.

Test corner cases when machine restarts and when it uses object states

If state is defined as object it may hold data from previous machine invocations.

Think if it should be cleared in some way.

Maybe machine may hold all mutable data, so it will be automatically dropped if state is used in new machine instance.

It is not clear if state may be mutated before adding to machine.

DSL is mutating the states

This machine has one fundamental problem, that you can not instantiate more than one instance of the State Machine, since it is not building its own immutable State graph based on the State definition, but the DSL is mutating the inner state of the State itself.

Kotlin 1.4 compatibility

0.18.0 -> 0.20.0 steped to kotlin 1.5, 0.18.0 was useful to me with 1.4 kotlin.
Still I was able to port 0.21.0 to 1.4 (tests are passing), and just want to clarify the further plans of the project. I assume, some 1.4 bugs and limits going to slow down project development and support.

Think about type safe argument passing

Currently argument passing is not type safe, but it seems to be a nice feature to pass data between states in type safe way

In this case state should declare types which it consumes and maybe provides.

Introduce choice state

I need to introduce choice state. Only role of such state is to check some conditions and conditionally redirect to another state.

Please check also plant uml documentation:
https://plantuml.com/state-diagram#8bd6f7be727fb20e

I know that check can be done in previous state, but unfortunately it is not always good idea. Often such state is some kind of hub where a lot states has transitions to (so logic must be duplicated in many states to avoid check state).

I have also a lot of such situations. I have several blocks of states. One block (which is a also the state) redirects to next block when child state has no transition to next state in the same block.

Sometimes blocks are conditional. So, after some initial check, choice state decides if internal states of block must be processed or to go to next block.

I implemented initial check like that:

fun IState.initialChoiceState(
    name: String? = null,
    block: UnitGuardedTransitionBuilder<Next>.() -> Unit
) {
    var argument: Any? = null
    onEntry {
        argument = it.argument
    }
    initialState("initialChoiceState${hashCode()}<<choice>>") {
        onEntry {
            machine.processEvent(Next, argument)
        }
        transition(name, block)
    }
}

And it used like that (some simplified case):

        addState(RBlock) {

            transition<Next>(targetState = DBlock)

            initialChoiceState {
                guard =
                    { someCheck() }
                targetState = A
            }

            addState(A) {
                transition<Next>(targetState = R)
            }

            addState(R)
        }
        
        addState(DBlock) {
...
      }


It works OK, but is not the best solution. Internally processEvent is called which typically throws an exception. I need to block the exception like that:

        pendingEventHandler = StateMachine.PendingEventHandler { pendingEvent, _ -> }

Question: could you implement some alternative way to make choice states, without such "hacks"? Sending next event in choice state is something which I want to avoid, just conditionally go to another state.

Problem using the DefaultDataState as initial state

I ran into a problem, if I use DefaultDataState as initial state (nested), the machine falls into error, when i change it to regular state (with no data), everything works fine

My data state:

    object DialogState1 : DefaultDataState<EmergencyEventData>("Dialog_State_1")

Add as initial state (nested):

 addInitialState(EmergencyDialogStates.DialogState1) {
                dataTransition<TimeoutEvent, EmergencyEventData> {
                    targetState = EmergencyDialogStates.DialogState2
                }
            }

the data event used:

class TimeoutEvent(override val data: EmergencyEventData) : DataEvent<EmergencyEventData>

Exception when entering the state:

2021-10-21 11:09:09.962 E/AndroidRuntime: FATAL EXCEPTION: DefaultDispatcher-worker-2
    Process: com.aloecare.hubv2.test, PID: 12584
    java.lang.IllegalStateException: ru.nsk.kstatemachine.BaseStateImpl$StartEvent@22d516f does not contain data required by DialogState1(name=Dialog_State_1)
        at ru.nsk.kstatemachine.DefaultDataState.onDoEnter(DefaultDataState.kt:19)
        at ru.nsk.kstatemachine.BaseStateImpl.doEnter(DefaultDataState.kt:116)
        at ru.nsk.kstatemachine.BaseStateImpl.notifyStateEntry(DefaultDataState.kt:252)
        at ru.nsk.kstatemachine.BaseStateImpl.setCurrentState(DefaultDataState.kt:241)
        at ru.nsk.kstatemachine.BaseStateImpl.recursiveEnterInitialStates(DefaultDataState.kt:178)
        at ru.nsk.kstatemachine.BaseStateImpl.recursiveEnterStatePath(DefaultDataState.kt:190)
        at ru.nsk.kstatemachine.BaseStateImpl.recursiveEnterStatePath(DefaultDataState.kt:196)
        at ru.nsk.kstatemachine.BaseStateImpl.switchToTargetState$kstatemachine(DefaultDataState.kt:272)
        at ru.nsk.kstatemachine.BaseStateImpl.doProcessEvent(DefaultDataState.kt:159)
        at ru.nsk.kstatemachine.StateMachineImpl.processEvent(StateMachineImpl.kt:80)
        at com.aloecare.hubv2.domain.flow.emergency.statemachine.EmergencyStateMachine.processEvent(EmergencyStateMachine.kt:209)
        at com.aloecare.hubv2.domain.base.StateMachineInterface$DefaultImpls.processEvent$default(StateMachineInterface.kt:13)
        at com.aloecare.hubv2.emergency.viewmodel.EmergencyViewModel$processEmergencyEvent$1.invokeSuspend(EmergencyViewModel.kt:450)
        at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
        at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
        at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
        at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
  • ver: 0.6.2

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.