Skip to content

Commit 49401c0

Browse files
committed
Make factory functions blocks suspendable. Update docs
1 parent 2dd8711 commit 49401c0

File tree

7 files changed

+87
-65
lines changed

7 files changed

+87
-65
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,12 +143,14 @@ The library consists of 2 artifacts:
143143
Add the dependency:
144144

145145
```groovy
146+
// groovy
146147
dependencies {
147148
implementation 'io.github.nsk90:kstatemachine:<Tag>'
148149
}
149150
```
150151

151152
```kotlin
153+
// kotlin
152154
dependencies {
153155
implementation("io.github.nsk90:kstatemachine:<Tag>")
154156
}
@@ -185,13 +187,19 @@ Add the dependency:
185187
// groovy
186188
dependencies {
187189
implementation 'com.github.nsk90:kstatemachine:<Tag>'
190+
// note that group is different in second artifact, long group name also works for first artifact but not vise versa
191+
// it is some strange JitPack behaviour
192+
implementation 'com.github.nsk90.kstatemachine:kstatemachine-coroutines:<Tag>' // optional
188193
}
189194
```
190195

191196
```kotlin
192197
// kotlin
193198
dependencies {
194199
implementation("com.github.nsk90:kstatemachine:<Tag>")
200+
// note that group is different in second artifact, long group name also works for first artifact but not vise versa
201+
// it is some strange JitPack behaviour
202+
implementation("com.github.nsk90.kstatemachine:kstatemachine-coroutines:<Tag>") // optional
195203
}
196204
```
197205

docs/index.md

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ machine.processEvent(YellowEvent)
2929
## Create state machine
3030

3131
First we create a state machine with one of those factory functions:
32+
3233
* `createStateMachine()` suspendable version (from `kstatemachine-coroutines` artifact)
3334
* `createStateMachineBlocking()` blocking version (from `kstatemachine-coroutines` artifact)
3435
* `createStdLibStateMachine()` - creates machine without Kotlin Coroutines support (from `kstatemachine` artifact)
@@ -42,7 +43,9 @@ val machine = createStateMachine(
4243
}
4344
```
4445

45-
By default, `createStateMachine()` starts state machine. You can control it using `start` argument.
46+
By default, factory functions start state machine. You can control it using `start` argument.
47+
48+
Subsequent samples will use `createStateMachine()` function, but you can choose that one which fits your needs.
4649

4750
## Setup states
4851

@@ -56,7 +59,7 @@ using [state subclasses](#state-subclasses).
5659
In state machine setup block define states with `initialState()`, `state()` and `finalState()` functions:
5760

5861
```kotlin
59-
createStateMachine {
62+
createStateMachine(scope) {
6063
// Use initialState() function to create initial State and add it to StateMachine
6164
// State machine enters this state after setup is complete
6265
val greenState = initialState()
@@ -71,7 +74,7 @@ createStateMachine {
7174
You can use `setInitialState()` function to set initial state separately:
7275

7376
```kotlin
74-
createStateMachine {
77+
createStateMachine(scope) {
7578
val greenState = state()
7679
setInitialState(greenState)
7780
// ...
@@ -87,7 +90,7 @@ Subclass `DefaultState`, `DefaultFinalState` or their [data](#typesafe-transitio
8790
```kotlin
8891
class SomeState : DefaultState()
8992

90-
createStateMachine {
93+
createStateMachine(scope) {
9194
val someState = addState(SomeState())
9295
// ...
9396
}
@@ -163,7 +166,7 @@ on an application business logic like with [conditional transitions](#conditiona
163166
and less flexibility:
164167

165168
```kotlin
166-
createStateMachine {
169+
createStateMachine(scope) {
167170
lateinit var yellowState: State
168171

169172
greenState {
@@ -217,7 +220,7 @@ There might be many transitions from one state to another. It is possible to lis
217220
setup block:
218221

219222
```kotlin
220-
createStateMachine {
223+
createStateMachine(scope) {
221224
// ...
222225
onTransition {
223226
// Listen to all triggered transitions here
@@ -343,7 +346,7 @@ You can enable internal state machine logging on your platform.
343346
On JVM:
344347

345348
```kotlin
346-
createStateMachine {
349+
createStateMachine(scope) {
347350
logger = StateMachine.Logger { lazyMessage ->
348351
println(lazyMessage())
349352
}
@@ -354,9 +357,9 @@ createStateMachine {
354357
On Android:
355358

356359
```kotlin
357-
createStateMachine {
358-
logger = StateMachine.Logger { lazyMessage ->
359-
Log.d(this::class.qualifiedName, lazyMessage())
360+
createStateMachine(scope) {
361+
logger = StateMachine.Logger { lazyMessage ->
362+
Log.d(this::class.qualifiedName, lazyMessage())
360363
}
361364
// ...
362365
}
@@ -391,7 +394,7 @@ Notifications about finishing are available in two forms:
391394
1. Triggering of `onFinished()` listener callback. This is the only option for `StateMachine`.
392395

393396
```kotlin
394-
val machine = createStateMachine {
397+
val machine = createStateMachine(scope) {
395398
val final = finalState("final")
396399
setInitialState(final)
397400

@@ -404,7 +407,7 @@ Notifications about finishing are available in two forms:
404407
for performing transitions on finishing:
405408

406409
```kotlin
407-
createStateMachine {
410+
createStateMachine(scope) {
408411
val state2 = state("state2")
409412

410413
initialState("state1") {
@@ -433,7 +436,7 @@ To create nested states simply use same functions (`state()`, `initialState()` e
433436
setup block:
434437

435438
```kotlin
436-
val machine = createStateMachine {
439+
val machine = createStateMachine(scope) {
437440
val topLevelState = initialState {
438441
// ...
439442
val nestedState = initialState {
@@ -459,7 +462,7 @@ A child state can override an inherited transition. To override parent transitio
459462
transition that matches the event.
460463

461464
```kotlin
462-
createStateMachine {
465+
createStateMachine(scope) {
463466
val state2 = state("state2")
464467
// all nested states inherit this parent transition
465468
transition<SwitchEvent> { targetState = state2 }
@@ -500,7 +503,7 @@ Set `childMode` argument of a state machine, or a state creation functions to `C
500503
with parallel child mode is entered or exited, all its child states will be simultaneously entered or exited:
501504

502505
```kotlin
503-
createStateMachine(childMode = ChildMode.PARALLEL) {
506+
createStateMachine(scope, childMode = ChildMode.PARALLEL) {
504507
state("Charger") {
505508
initialState("Charging") { /* ... */ }
506509
state("OnBattery") { /* ... */ }
@@ -539,7 +542,7 @@ You can specify default state which will be used if history was not recorded yet
539542
When default state is not specified, parent initial state will be entered on transition to history state.
540543
541544
```kotlin
542-
val machine = createStateMachine {
545+
val machine = createStateMachine(scope) {
543546
state {
544547
val state11 = initialState()
545548
val state12 = state()
@@ -562,7 +565,7 @@ from defining a transition with incompatible data type parameters of event and t
562565
```kotlin
563566
class StringEvent(override val data: String) : DataEvent<String>
564567
565-
createStateMachine {
568+
createStateMachine(scope) {
566569
val state2 = dataState<String> {
567570
onEntry { println("State data: $data") }
568571
}
@@ -602,7 +605,7 @@ simpler to use event argument. You can specify arbitrary argument with an event
602605
can get this argument in a state and transition listeners.
603606

604607
```kotlin
605-
val machine = createStateMachine {
608+
val machine = createStateMachine(scope) {
606609
state("offState").onEntry {
607610
println("Event ${it.event} argument: ${it.argument}")
608611
}
@@ -639,7 +642,7 @@ By default, state machine simply ignores events that does not match any defined
639642
logging is enabled or use custom `IgnoredEventHandler` for example to throw error:
640643

641644
```kotlin
642-
createStateMachine {
645+
createStateMachine(scope) {
643646
// ...
644647
ignoredEventHandler = StateMachine.IgnoredEventHandler {
645648
error("unexpected ${it.event}")
@@ -662,7 +665,7 @@ thrown. Alternatively with custom `PendingEventHandler` you can post such events
662665
passing to `processEvent()`. Using of throwing `PendingEventHandler` sample:
663666

664667
```kotlin
665-
createStateMachine {
668+
createStateMachine(scope) {
666669
// ...
667670
pendingEventHandler = throwingPendingEventHandler()
668671
}
@@ -687,38 +690,47 @@ Calling `processEvent()` on destroyed machine will throw also.
687690

688691
## Multithreading and concurrency
689692

690-
KStateMachine is designed to work in single thread.
693+
KStateMachine is designed to work in single thread.
691694
Concurrent modification of library classes will lead to race conditions.
692695

693696
## Kotlin Coroutines
694697

695698
Starting from `KStateMachine v0.20.0` the library has built-in coroutines support.
696699
All its callbacks and other APIs were marked with `suspend` modifier, allowing to use coroutines from them.
697-
You can still use all KStateMachine features without Kotlin Coroutines library dependency as `suspend` keyword
700+
You can still use all KStateMachine features without Kotlin Coroutines library dependency as `suspend` keyword
698701
is implemented at compiler level and Coroutines library is not really necessary to start coroutines.
699702

700-
TODO work in progress
703+
Many functions like `createStateMachine`/`start`/`stop`/`processEvent`/`undo` etc. are suspendable, but all of them
704+
has analogs with `Blocking` suffix which are not marked with `suspend` keyword.
705+
If you use KStateMachine with coroutines support you should prefer suspendable function versions.
706+
Note that `Blocking` versions internally use `kotlinx.coroutines.runBlocking` function which is rather dangerous and
707+
may cause deadlocks if used not properly. That is why you should avoid using `Blocking` APIs from coroutines and
708+
recursively (from library callbacks).
709+
710+
Such suspendable functions preserve state machines coroutine context (using `kotlinx.coroutines.withContext`),
711+
so it should be ok to call them from any thread.
701712

702713
### Migration guide from versions older than v0.20.0
703714

704715
#### If you already have or ready to add Kotlin Coroutines dependency
705716

706717
* Add both `kstatemachine` and `kstatemachine-coroutines` artifacts to your build system
707-
* Use `createStateMachine` from `kstatemachine-coroutines` artifact to create state machines
708-
providing `CoroutineScope` as argument
709-
* Use suspendable versions of functions (`start`/`stop`/`processEvent` etc.) when possible
718+
* Use `createStateMachine` or `createStateMachineBlocking` from `kstatemachine-coroutines` artifact to create state
719+
machines providing `CoroutineScope` as argument
720+
* Use suspendable versions of functions (`start`/`stop`/`processEvent`/`undo` etc.) when possible
710721
* Avoid using function analogs with `Blocking` suffix **(especially recursively)** as this may easily lead to deadlocks
711-
or race conditions depending on your use case and machine configuration
722+
or race conditions depending on your use case and machine configuration
712723

713724
#### If you can not have dependency on Kotlin Coroutines or just do not want to use it
714725

715726
* Use only `kstatemachine` artifact in your build system
716727
* Use `createStdLibStateMachine` to create state machines
717-
* Use suspendable versions of functions (`start`/`stop`/`processEvent` etc.) when possible (from KStateMachine callbacks)
728+
* Use suspendable versions of functions (`start`/`stop`/`processEvent`/`undo` etc.) when possible
729+
(from KStateMachine callbacks for example)
718730
* In other cases use their analogs with `Blocking` suffix, it is ok
719731
* If you try to use Kotlin Coroutines library from machine created by `createStdLibStateMachine` you will probably get
720-
an exception.
721-
* Using suspendable code without calls to Kotlin Coroutines library is ok, as `suspend` keyword is a compiler feature,
732+
an exception.
733+
* Using suspendable code without calls to Kotlin Coroutines library is ok, as `suspend` keyword is a compiler feature,
722734
not library one.
723735

724736
## Export
@@ -729,11 +741,11 @@ may touch application data that is not valid when export is running._
729741

730742
### PlantUML
731743

732-
Use `exportToPlantUml()/exportToPlantUmlBlocking()` extension function to export state machine
744+
Use `exportToPlantUml()`/`exportToPlantUmlBlocking()` extension function to export state machine
733745
to [PlantUML state diagram](https://plantuml.com/en/state-diagram).
734746

735747
```kotlin
736-
val machine = createStateMachine { /* ... */ }
748+
val machine = createStateMachine(scope) { /* ... */ }
737749
println(machine.exportToPlantUml())
738750
```
739751

@@ -744,12 +756,13 @@ See [PlantUML nested states export sample](https://github.com/nsk90/kstatemachin
744756
## Testing
745757

746758
For testing, it might be useful to check how state machine reacts on events from particular state. There
747-
are several `Testing.startFrom()` overloaded functions which allow starting the machine from a specified state:
759+
are several `Testing.startFrom()`/`Testing.startFromBlocking()` overloaded functions which allow starting the machine
760+
from a specified state:
748761

749762
```kotlin
750763
lateinit var state2: State
751764

752-
val machine = createStateMachine(start = false) {
765+
val machine = createStateMachine(scope, start = false) {
753766
initialState("state1")
754767
state2 = state("state2")
755768
// ...
@@ -802,7 +815,8 @@ Correct - let the state machine to make decisions on an event:
802815
machine.processEvent(SomethingHappenedEvent)
803816
```
804817

805-
In certain scenarios (like a `state pattern` maybe) it is fine to use events like some kind of _setState() / goToState()_
818+
In certain scenarios (like a `state pattern` maybe) it is fine to use events like some kind of _setState() /
819+
goToState()_
806820
functions but in general it is wrong, as events are not commands.
807821

808822
## Known issues

kstatemachine-coroutines/src/main/kotlin/ru/nsk/kstatemachine/CoroutinesStateMachine.kt

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,36 @@ package ru.nsk.kstatemachine
33
import kotlinx.coroutines.CoroutineScope
44

55
/**
6-
* Analog of [createStdLibStateMachine] function, with Kotlin Coroutines support.
6+
* Suspendable analog of [createStdLibStateMachine] function, with Kotlin Coroutines support.
77
* This is preferred function. Use this one especially if you are going to use Kotlin Coroutines library from
88
* KStateMachine callbacks.
99
*
1010
* @param scope be careful while working with threaded scopes as KStateMachine classes are not thread-safe.
1111
* Usually you should use only single threaded scopes, for example:
12-
*
12+
*
1313
* CoroutineScope(Dispatchers.Default.limitedParallelism(1))
1414
*
15-
* Note that all calls to this machine instance should be done only from that thread.
15+
* Note that all calls to created machine instance should be done only from that thread.
1616
*/
17+
suspend fun createStateMachine(
18+
scope: CoroutineScope,
19+
name: String? = null,
20+
childMode: ChildMode = ChildMode.EXCLUSIVE,
21+
start: Boolean = true,
22+
autoDestroyOnStatesReuse: Boolean = true,
23+
enableUndo: Boolean = false,
24+
doNotThrowOnMultipleTransitionsMatch: Boolean = false,
25+
init: suspend BuildingStateMachine.() -> Unit
26+
) = CoroutinesLibCoroutineAbstraction(scope).createStateMachine(
27+
name,
28+
childMode,
29+
start,
30+
autoDestroyOnStatesReuse,
31+
enableUndo,
32+
doNotThrowOnMultipleTransitionsMatch,
33+
init
34+
)
35+
1736
fun createStateMachineBlocking(
1837
scope: CoroutineScope,
1938
name: String? = null,
@@ -22,7 +41,7 @@ fun createStateMachineBlocking(
2241
autoDestroyOnStatesReuse: Boolean = true,
2342
enableUndo: Boolean = false,
2443
doNotThrowOnMultipleTransitionsMatch: Boolean = false,
25-
init: BuildingStateMachine.() -> Unit
44+
init: suspend BuildingStateMachine.() -> Unit
2645
) = with(CoroutinesLibCoroutineAbstraction(scope)) {
2746
runBlocking {
2847
createStateMachine(
@@ -35,23 +54,4 @@ fun createStateMachineBlocking(
3554
init
3655
)
3756
}
38-
}
39-
40-
suspend fun createStateMachine(
41-
scope: CoroutineScope,
42-
name: String? = null,
43-
childMode: ChildMode = ChildMode.EXCLUSIVE,
44-
start: Boolean = true,
45-
autoDestroyOnStatesReuse: Boolean = true,
46-
enableUndo: Boolean = false,
47-
doNotThrowOnMultipleTransitionsMatch: Boolean = false,
48-
init: BuildingStateMachine.() -> Unit
49-
) = CoroutinesLibCoroutineAbstraction(scope).createStateMachine(
50-
name,
51-
childMode,
52-
start,
53-
autoDestroyOnStatesReuse,
54-
enableUndo,
55-
doNotThrowOnMultipleTransitionsMatch,
56-
init
57-
)
57+
}

kstatemachine/src/main/kotlin/ru/nsk/kstatemachine/CoroutineAbstraction.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ suspend fun CoroutineAbstraction.createStateMachine(
4949
autoDestroyOnStatesReuse: Boolean,
5050
enableUndo: Boolean,
5151
doNotThrowOnMultipleTransitionsMatch: Boolean,
52-
init: BuildingStateMachine.() -> Unit
52+
init: suspend BuildingStateMachine.() -> Unit
5353
): StateMachine = StateMachineImpl(
5454
name,
5555
childMode,

0 commit comments

Comments
 (0)