Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions .idea/markdown-navigator-enh.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 55 additions & 0 deletions .idea/markdown-navigator.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 23 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ allprojects {
At a module level `build.gradle`, add the following dependency:

```groovy
implementation 'vit.khudenko.android:fsm:0.3.0'
implementation 'vit.khudenko.android:fsm:0.4.0'
```

### Usage
Expand All @@ -26,7 +26,7 @@ A sample configuration, assuming your app has `Session` class, that defines __sp
to your app__ events and states (`Session.Event` and `Session.State` enums):

```kotlin
val sessionStateMachine = StateMachine.Builder<Session.Event, Session.State>()
val sessionStateMachine = StateMachine.Builder<Session.Event, Session.State, Unit>()
.setInitialState(Session.State.ACTIVE)
.addTransition(
StateMachine.Transition(
Expand Down Expand Up @@ -71,13 +71,32 @@ sessionStateMachine.consumeEvent(Session.Event.LOGOUT)
State changes are propagated via `StateMachine.Listener`.

```kotlin
sessionStateMachine.addListener(object : StateMachine.Listener<Session.State> {
override fun onStateChanged(oldState: Session.State, newState: Session.State) {
sessionStateMachine.addListener(object : StateMachine.Listener<Session.State, Unit> {
override fun onStateChanged(oldState: Session.State, newState: Session.State, eventData: Unit?) {
// do something
}
})
```

#### Event payloads

Starting from v0.4.0 `StateMachine` supports event payloads, i.e. in addition to bare `enum` instances serving as events the library supports event extension/payloads:

```kotlin
data class MyEventPayload(val value: Int)

val stateMachine: StateMachine<MyEvent, MyState, MyEventPayload> // instantiation is omitted for brevity

stateMachine.addListener(object : StateMachine.Listener<MyState, MyEventPayload> {
override fun onStateChanged(oldState: MyState, newState: MyState, eventData: MyEventPayload?) {
// do something
}
})

stateMachine.consumeEvent(MyEvent.A) // equivalent to stateMachine.consumeEvent(MyEvent.A, null)
stateMachine.consumeEvent(MyEvent.B, MyEventPayload(47))
```

### Threading

The `StateMachine` implementation is thread-safe. Public API methods are declared as `synchronized`.
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
ext.kotlin_version = '1.3.61'
ext.kotlin_version = '1.3.70'
repositories {
google()
jcenter()
Expand Down
2 changes: 1 addition & 1 deletion state_machine_lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ afterEvaluate {

apply plugin: 'kotlin-android'

def libraryVersionName = "0.3.0"
def libraryVersionName = "0.4.0"
def artifactName = 'StateMachine'

android {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package vit.khudenko.android.fsm

import java.util.*
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
import kotlin.collections.LinkedHashSet
import java.util.Collections
import kotlin.collections.set

/**
Expand Down Expand Up @@ -40,25 +37,26 @@ import kotlin.collections.set
*
* @param [Event] event parameter of enum type.
* @param [State] state parameter of enum type.
* @param [EventPayload] event payload parameter.
*
* @see [StateMachine.Builder]
*/
class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor(
class StateMachine<Event : Enum<Event>, State : Enum<State>, EventPayload> private constructor(
private val graph: Map<Transition.Identity<Event, State>, List<State>>,
initialState: State
) {

/**
* A callback to communicate state changes of a [`StateMachine`][StateMachine].
*/
interface Listener<State> {
fun onStateChanged(oldState: State, newState: State)
interface Listener<State, EventPayload> {
fun onStateChanged(oldState: State, newState: State, eventPayload: EventPayload?)
}

/**
* Builder is not thread-safe.
*/
class Builder<Event : Enum<Event>, State : Enum<State>> {
class Builder<Event : Enum<Event>, State : Enum<State>, EventPayload> {

private val graph = HashMap<Transition.Identity<Event, State>, List<State>>()
private lateinit var initialState: State
Expand All @@ -76,7 +74,7 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
* @throws [StateMachineBuilderValidationException] if a duplicate transition identified (by a combination
* of event and starting state)
*/
fun addTransition(transition: Transition<Event, State>): Builder<Event, State> {
fun addTransition(transition: Transition<Event, State>): Builder<Event, State, EventPayload> {
val statePathCopy = transition.statePath.toMutableList()
val startState = statePathCopy.removeAt(0)

Expand All @@ -96,7 +94,7 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
*
* @return [`StateMachine.Builder`][StateMachine.Builder]
*/
fun setInitialState(state: State): Builder<Event, State> {
fun setInitialState(state: State): Builder<Event, State, EventPayload> {
this.initialState = state
return this
}
Expand All @@ -109,7 +107,7 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
* @throws [StateMachineBuilderValidationException] if no transition defined with starting state matching
* the initial state
*/
fun build(): StateMachine<Event, State> {
fun build(): StateMachine<Event, State, EventPayload> {
if (this::initialState.isInitialized.not()) {
throw StateMachineBuilderValidationException(
"initial state is not defined, make sure to call ${StateMachine::class.java.simpleName}" +
Expand All @@ -132,7 +130,7 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
}

private var currentState: State = initialState
private val listeners: LinkedHashSet<Listener<State>> = LinkedHashSet()
private val listeners: LinkedHashSet<Listener<State, EventPayload>> = LinkedHashSet()
private var inTransition: Boolean = false

/**
Expand All @@ -141,7 +139,7 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
* If this [`listener`][listener] has been already added, then this call is no op.
*/
@Synchronized
fun addListener(listener: Listener<State>) {
fun addListener(listener: Listener<State, EventPayload>) {
if (!listeners.contains(listener)) {
listeners.add(listener)
}
Expand All @@ -159,7 +157,7 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
* Removes [`listener`][listener] from this `StateMachine`.
*/
@Synchronized
fun removeListener(listener: Listener<State>) {
fun removeListener(listener: Listener<State, EventPayload>) {
listeners.remove(listener)
}

Expand All @@ -174,14 +172,15 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
* State changes are communicated via the [`StateMachine.Listener`][StateMachine.Listener] listeners.
*
* @param event [`Event`][Event]
* @param eventPayload [`EventPayload`][EventPayload] optional payload associated with the [event]
*
* @return flag of whether the event was actually consumed (meaning moving to a new state) or ignored.
*
* @throws [IllegalStateException] if there is a matching transition for this event and current state,
* but there is still an unfinished transition in progress.
*/
@Synchronized
fun consumeEvent(event: Event): Boolean {
fun consumeEvent(event: Event, eventPayload: EventPayload? = null): Boolean {
val transitionIdentity = Transition.Identity(event, currentState)
val transition = graph[transitionIdentity] ?: return false

Expand All @@ -194,7 +193,7 @@ class StateMachine<Event : Enum<Event>, State : Enum<State>> private constructor
val newState = transition[i]
currentState = newState
for (listener in ArrayList(listeners)) {
listener.onStateChanged(oldState, newState)
listener.onStateChanged(oldState, newState, eventPayload)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
"no transitions defined, make sure to call StateMachine.Builder.addTransition()"
) {
StateMachine.Builder<Event, State>()
StateMachine.Builder<Event, State, Unit>()
.setInitialState(STATE_A)
.build()
}
Expand All @@ -34,7 +34,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
"initial state is not defined, make sure to call StateMachine.Builder.setInitialState()"
) {
StateMachine.Builder<Event, State>()
StateMachine.Builder<Event, State, Unit>()
.addTransition(transition)
.build()
}
Expand All @@ -52,7 +52,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
getCauseForDuplicateStartState(event, state)
) {
StateMachine.Builder<Event, State>()
StateMachine.Builder<Event, State, Unit>()
.addTransition(transition1)
.addTransition(transition2)
}
Expand All @@ -70,7 +70,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
getCauseForDuplicateStartState(event, state)
) {
StateMachine.Builder<Event, State>()
StateMachine.Builder<Event, State, Unit>()
.addTransition(transition1)
.addTransition(transition2)
}
Expand All @@ -88,7 +88,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
getCauseForDuplicateStartState(event, state)
) {
StateMachine.Builder<Event, State>()
StateMachine.Builder<Event, State, Unit>()
.addTransition(transition1)
.addTransition(transition2)
}
Expand All @@ -103,7 +103,7 @@ class StateMachineBuilderTest {
StateMachineBuilderValidationException::class.java,
"no transition defined with start state matching the initial state ($initialState)"
) {
StateMachine.Builder<Event, State>()
StateMachine.Builder<Event, State, Unit>()
.setInitialState(initialState)
.addTransition(transition)
.build()
Expand All @@ -119,7 +119,7 @@ class StateMachineBuilderTest {

val state = STATE_A

val stateMachine = StateMachine.Builder<Event, State>()
val stateMachine = StateMachine.Builder<Event, State, Unit>()
.addTransition(transition1)
.addTransition(transition2)
.addTransition(transition3)
Expand Down
Loading