All Projects â†’ afollestad â†’ State Machine

afollestad / State Machine

Licence: apache-2.0
🤖 A state machine library for Kotlin, with extensions for Android.

Programming Languages

kotlin
9241 projects

Projects that are alternatives of or similar to State Machine

Lastik
Kotlin Multiplatform + Jetpack Compose pet project, based on www.last.fm/api (in development)
Stars: ✭ 37 (-48.61%)
Mutual labels:  jvm, coroutines
esm
Lightweight communicating state machine framework for embedded systems
Stars: ✭ 21 (-70.83%)
Mutual labels:  reactive, state-machine
delta
DDD-centric event-sourcing library for the JVM
Stars: ✭ 15 (-79.17%)
Mutual labels:  reactive, jvm
rxjava2-http
Transmit RxJava2 Flowable over http with non-blocking backpressure
Stars: ✭ 19 (-73.61%)
Mutual labels:  reactive, jvm
Korge
KorGE Game Engine. Multiplatform Kotlin Game Engine
Stars: ✭ 780 (+983.33%)
Mutual labels:  coroutines, jvm
UnityHFSM
A simple yet powerful class based hierarchical finite state machine for Unity3D
Stars: ✭ 243 (+237.5%)
Mutual labels:  state-machine, coroutines
kit
C++11 libs: await, channels, reactive/signals, timelines, alarms, logging, args, etc.
Stars: ✭ 21 (-70.83%)
Mutual labels:  reactive, coroutines
Vert.x
Vert.x is a tool-kit for building reactive applications on the JVM
Stars: ✭ 12,544 (+17322.22%)
Mutual labels:  reactive, jvm
Reactor Core
Non-Blocking Reactive Foundation for the JVM
Stars: ✭ 3,891 (+5304.17%)
Mutual labels:  reactive, jvm
Beedle
A tiny library inspired by Redux & Vuex to help you manage state in your JavaScript apps
Stars: ✭ 329 (+356.94%)
Mutual labels:  reactive, state-machine
M2A01 MuSimpron
Small yet powerful state machine coroutine library
Stars: ✭ 34 (-52.78%)
Mutual labels:  state-machine, coroutines
Ktor Rest Sample
Sample REST Service written in Kotlin with Ktor Framework
Stars: ✭ 37 (-48.61%)
Mutual labels:  reactive, coroutines
DemOS
Free, simple, extremely lightweight, stackless, cooperative, co-routine system (OS) for microcontrollers
Stars: ✭ 18 (-75%)
Mutual labels:  state-machine, coroutines
statechart
A rust implementation of statecharts: hierarchical, reactive state machines
Stars: ✭ 41 (-43.06%)
Mutual labels:  reactive, state-machine
Smallrye Mutiny
An Intuitive Event-Driven Reactive Programming Library for Java
Stars: ✭ 231 (+220.83%)
Mutual labels:  reactive, jvm
GitKtDroid
A sample Android application📱 built with Kotlin for #30DaysOfKotlin
Stars: ✭ 53 (-26.39%)
Mutual labels:  reactive, coroutines
Orbit
Orbit - Virtual actor framework for building distributed systems
Stars: ✭ 1,585 (+2101.39%)
Mutual labels:  reactive, jvm
Playframework
Play Framework
Stars: ✭ 12,041 (+16623.61%)
Mutual labels:  reactive, jvm
Workflow Kotlin
A Swift and Kotlin library for making composable state machines, and UIs driven by those state machines.
Stars: ✭ 255 (+254.17%)
Mutual labels:  reactive, state-machine
Workflow
A Swift and Kotlin library for making composable state machines, and UIs driven by those state machines.
Stars: ✭ 860 (+1094.44%)
Mutual labels:  reactive, state-machine

State Machine

Build Status License

Table of Contents

  1. States and Events
  2. Building a Machine
    1. Gradle Dependency
    2. Basics
    3. Usage
  3. Using a Machine on Android
    1. Gradle Dependency
    2. States and View Models
    3. Dispatchers
    4. Usage
  4. Unit Testing Machines
    1. Gradle Dependency
    2. Usage

States and Events

A state machine takes three generic parameters: the events it takes in, the states it spits out, and the arguments it finishes with. These could be sealed classes, or plain classes & objects.

sealed class Event {
  data class InputText(val text: String) : Event()
  
  object ClickedButton : Event()
}

sealed class State {
  abstract val showProgress: Boolean

  data class Idle(val currentInput: String = "") : State() {
     override val showProgress: Boolean = false
  }
  
  data class Submitting(val currentInput: String) : State() {
     override val showProgress: Boolean = true
  }
  
  object Success : State() {
     override val showProgress: Boolean = false
  }
  
  data class Failure(val reason: String) : State() {
     override val showProgress: Boolean = false
  }
}

data class Result(val success: Boolean)

Building a Machine

A basic machine only requires the core module without any extensions.

Gradle Dependency

Core

dependencies {
   ...
   implementation "com.afollestad:statemachine:0.0.1-alpha1"
}

Basics

Your state machine should be a subclass of the StateMachine class.

When the react method is called, your machine should setup handlers for states. Each state handles a set of events, each event returns a state. The state should propagate to the UI, and the UI should send in events when there is interaction. It's a loop, which is basically what a state machine is.

class MyStateMachine : StateMachine<Event, State, Result>() {
  override suspend fun react() {
    onState<Idle> {
      onEvent<InputText> {
        state.copy(currentInput = event.text)
      }
      onEvent<ClickedButton> {
        Submitting(currentInput = state.currentInput)
      }
    }
    onState<Submitting> {
      onEnter { doServerWork(state) }
    }
    onState<Success> {
      onEnter {
        delay(2000) // A synthetic delay
        finish(Result(true)) // Kills the state machine.
      }
    }
    onState<Failure> {
      onEnter {
        delay(5000) 
        Idle() 
      }
    }
  }

  private suspend fun doServerWork(state: Submitting): State {
    delay(2000)
    return if (state.currentInput == "fail") {
      Failure(reason = "Your wish is my command.")
    } else {
      Success
    }
  }
}

Usage

This example below demonstrates usage from a JVM command-line program.

fun main() {
  val stateMachine = MyStateMachine()
  
  // Listen for state changes in the background
  GlobalScope.launch {
    stateMachine.stateFlow()
        .collect { state ->
          println("Current state: $state")
        }
  }
  
  // Start the machine with an initial state, launches the event looper.
  stateMachine.start(Idle()) { args ->
    // This callback is invoked when the machine is finished.
    // It cannot be re-used after this point.
    println("Goodbye.")
  }

  val inputReader = Scanner(System.`in`)
  while (stateMachine.isActive) {
    when (val input = inputReader.nextLine()) {
      "send" -> {
        // The invoke operator on the machine streams in events.
        stateMachine(ClickedButton)
      }
      "exit" -> {
        // The finish() method destroys the state machine.
        // This releases resources and shuts down the event looper.
        stateMachine.finish()
      }
      else -> stateMachine(InputText(input))
    }
  }
}

Using a Machine on Android

On Android, there are extension modules that you can use to make connecting your states and UI much easier. Annotation processing is used to generate view models from your state.

Gradle Dependency

Core Android

On Android, your application should depend on the core-android module as an implementation dependency, and the core-android-processor module as an annotation processor (with kapt).

dependencies {
   ...
   implementation "com.afollestad:statemachine-android:0.0.1-alpha1"
   kapt "com.afollestad:statemachine-android-processor:0.0.1-alpha1"
}

States and View Models

Your state must be tagged with the @StateToViewModel annotation. Only the parent class should be tagged.

@StateToViewModel
sealed class MyState {
  abstract val buttonEnabled: Boolean

  data class Idle(val currentInput: String = "") : State() {
     override val buttonEnabled = true
  }
  data class Submitting(val currentInput: String) : State() {
     override val buttonEnabled = false
  }
  object Success : State() {
     override val buttonEnabled = false
  }
  data class Failure(val errorMessage: String) : State() {
     override val buttonEnabled = true
  }
}

This will generate a ViewModel that you can use in your UI. From the above state, you'd get something similar to this (some details are omitted because they aren't relevant):

class MyStateViewModel : ViewModel(), StateConsumer<MyState> {
  val onButtonEnabled: LiveData<Boolean> = // ...
  val onCurrentInput: LiveData<String> = // ...
  val onErrorMessage: LiveData<String> = // ...
  
  override fun accept(state: MyState) {
    ...
     // This is filled automatically to propagate state changes into the live data fields above.
  }
}

Dispatchers

On Android, being thread conscious is important. StateMachine's constructor takes two coroutine dispatchers - one for background execution, and one for the main thread.

class MyStateMachine : StateMachine<Event, State, Result>(
   executionContext = Dispatchers.IO,
   mainContext = Dispatchers.Main
) {
  ...
}

Usage

In your Activity, Fragment, etc. you can use the generated ViewModels like you would without this library.

class MyActivity : AppCompatActivity() {
  private lateinit var button: Button
  private lateinit var input: EditText
  private lateinit var error: TextView

  private lateinit var stateMachine: MyStateMachine

  override fun onCreate(savedInstanceState: Bundle?) {
      super.onCreate(savedInstanceState)
      // ...
      
      stateMachine = MyStateMachine()
      
      val viewModel = ViewModelProviders.of(this)
         .get(MyStateViewModel::class.java)
      
      viewModel.onButtonEnabled
         .observe(this, button::setEnabled)
      viewModel.onCurrentInput
         /** Filtering here is important to we don't end up with an input/output loop */
         .filter { it != input.text.toString() }
         .observe(this, input::setText)
      viewModel.onErrorMessage
         .observe(this, error::setTextAndVisibility)
          
      input.onTextChanged { text -> 
         stateMachine(InputText(text = text)) 
      }
      button.setOnClickListener { 
         stateMachine(ClickedSubmit) 
      }
      
      // This is important on Android! This will automatically finish your state machine
      // when `this` (a LifecycleOwner) is destroyed. You could of course use plain `start()` 
      // here and remember to call `finish` somewhere below later, e.g. on `onDestroy()`.
      startMachine.startWithOwner(this)
  }
  
  private fun TextView.setTextAndVisibility(text: String) {
     setText(text)
     visibility = if (text.isNotEmpty()) VISIBLE else GONE
  }
}

Your UI will automatically receive propagated values from your states as they come in. The generated view model will automatically distinct values as well, so you won't receive duplicate emissions (which could result in unnecessary UI invalidation).

If you're familiar with LiveData, you'll also know that this implementation is totally lifecycle safe. LiveData will not emit if the lifecycle owner is no longer started.


Unit Testing Machines

A core-test module is provided to aid in unit testing your state machines.

Gradle Dependency

Core Testing

dependencies {
   ...
   testImplementation "com.afollestad:statemachine-test:0.0.1-alpha1"
}

Usage

Here's a simple example of a JUnit test, for the state machine above.

class MyTests {
  // The core-test module exposes a `test()` extension method.
  private val stateMachine = MyStateMachine().test()
  
  @Test fun `idle state accepts input`() {
     val initialState = Idle()
     stateMachine.start(initialState)
     // send an event in
     stateMachine(InputText(text))
     // assert the initial state and new state
     stateMachine.assertStates(initialState, Idle(text))
  }
  
  @Test fun `click button in idle submits`() {
     val initialState = Idle("hello world")
     stateMachine.start(initialState)
     // send an event in
     stateMachine(ClickedButton)
     // assert the initial state and new states
     // Submitting's `onEnter` brings us to the Success state.
     stateMachine.assertStates(initialState, Submitting(text), Success)
  }
  
  @After fun shutdown() {
     // Ensure resources are released and the event looper is stopped.
     stateMachine.destroy()
  }
}
Note that the project description data, including the texts, logos, images, and/or trademarks, for each open source project belongs to its rightful owner. If you wish to add or remove any projects, please contact us at [email protected].