Composing state machines (hierarchical state machines)¶
With FlowRedux you can compose state machines from other state machines.
This concept is called hierarchical state machines.
In this section we will introduce onActionStartStateMachine() and onEnterStartStateMachine().
Think about Jetpack Compose, SwiftUI or React.
They are declarative frameworks to build UI.
Furthermore, you are encourage by these frameworks to build reusable UI components.
Wouldn’t it be great to get the same for your business logic?
With FlowRedux’s onActionStartStateMachine() and onEnterStartStateMachine() you can do that.
Advantages: - reuse state machines while still keep them decoupled and encapsulated from each other - favor composition over inheritance - easier to test
onActionStartStateMachine()¶
Let’s continue enhancing our ItemListStateMachine.
ShowContent state is defined as following:
data class ShowContent(val items: List<Item>) : ListState
data class Item(val id : Int, val name : String)
Let’s say we want to have the option to mark an Item as favorite (and also remove an Item as favorite).
The favorite items are actually saved on a server and we communicate with it over http.
Let’s extend Item class to model this new requirements:
data class Item(
val id : Int,
val name : String,
val favoriteStatus : FavoriteStatus
)
sealed interface FavoriteStatus {
val itemId : Int
// It is not marked as favorite yet
data class NotFavorite(override val itemId : Int) : FavoriteStatus
// Marked as favorites
data class Favorite(override val itemId : Int) : FavoriteStatus
// An operation (read: http request) is in progress to either mark
// it as favorite or not mark it as favorite
data class OperationInProgress(
override val itemId : Int,
val markAsFavorite : Boolean // true means mark as favorite, false means unmark it
) : FavoriteStatus
// The operation (read: http request) to either mark it as favorite
// or unmark it as favorite has failed; so did not succeed.
data class OperationFailed(
override val itemId : Int,
val markAsFavorite : Boolean // true means mark as favorite, false means unmark it
) : FavoriteStatus
}
You may wonder why we need FavoriteStatus and why it is not just a Boolean to reflect marked as favorite or not?
Remember: we also need to talk to a server (via http) whenever the user wants to mark an Item as favorite or unmark it.
The UI looks like this:

Let’s for now ignore the ItemListStateMachine and only focus on our new requirements: marking an Item as favorite (or unmark it) plus the communication with our backend server to store that information.
We could add this new requirements with our DSL to ItemListStateMachine somehow or we extract that into a small stand alone state machine.
Let’s call this state machine FavoriteStatusStateMachine and use the FlowRedux DSL to define it’s logic.
Additionally, let’s say when an network error in the communication with the backend server happened we will show an error for 3 seconds and then reset back to either maked as favorite or not.
class FavoriteStatusStateMachine(
item : Item,
private val httpClient : HttpClient
) : FlowReduxStateMachine<FavoriteStatus, Nothing>( // doesn't handle Action, thus we can use Nothing
initialState = OperationInProgress(
itemId = item.itemId,
markAsFavorite = item.favoriteStatus is NotFavorite
)
) {
init {
spec {
inState<OperationInProgress>{
onEnter { state ->
toggleFavoriteAndSaveToServer(state)
}
}
inState<OperationFailed>{
onEnter{ state ->
waitFor3SecondsThenResetToOriginalState(state)
}
}
}
}
private suspend fun toggleFavoriteAndSaveToServer(
state : State<OperationInProgress>
) : ChangedState<FavoriteStatus>{
return try {
val itemId = state.snapshot.itemId
val markAsFavorite = state.snapshot.markAsFavorite
httpClient.toggleFavorite(
itemId = itemId,
markAsFavorite = markAsFavorite // if false then unmark it, if true mark it as favorite
)
if (markAsFavorite)
state.override { Favorite(itemId) }
else
state.override { NotFavorite(itemId) }
} catch(exception : Throwable){
state.override { OperationFailed(itemId, markAsFavorite) }
}
}
private suspend fun waitFor3SecondsThenResetToOriginalState(
state : State<OperationFailed>
) : ChangedState<FavoriteStatus> {
delay(3_000) // wait for 3 seconds
val itemId = state.snapshot.itemId
val markAsFavorite = state.snapshot.markAsFavorite
return if (markAsFavorite)
// marking as favorite failed,
// thus original status was "not marked as favorite"
state.override { NotFavorite(itemId) }
else
state.override { Favorite(itemId) }
}
}
All that FavoriteStatusStateMachine does is making an http request to the backend and in case of error reset back to the previous state after showing an error state for 3 seconds.
This is how the UI should looks like:

Now let’s connect this with our ItemListStateMachine by using onActionStartStateMachine().
data class ToggleFavoriteItemAction(val itemId : Int) : Action
class ItemListStateMachine(
private val httpClient: HttpClient
) : FlowReduxStateMachine<ListState, Action>(initialState = Loading) {
// This is the specification of your state machine.
// Less implementation details, better readability.
init {
spec {
inState<Loading> {
onEnter { loadItemsAndMoveToContentOrError(it) }
}
inState<Error> {
on<RetryLoadingAction> { action, state ->
state.override { Loading }
}
collectWhileInState(timerThatEmitsEverySecond()) { value, state ->
decrementCountdownAndMoveToLoading(value, state)
}
}
// NEW DSL block
inState<ShowContent> {
// on ToggleFavoriteItemAction start statemachine
onActionStartStateMachine(
stateMachineFactory = {
action: ToggleFavoriteItemAction, stateSnapshot : ShowContent ->
val item : Item = stateSnapshot.items.find { it == action.itemId}
// create and return a new FavoriteStatusStateMachine instance
FavoriteStatusStateMachine(item, httpClient)
},
stateMapper = {
itemListState : State<ShowContent>, favoriteStatus :FavoriteStatus ->
itemListState.mutate {
val itemToRepace : Item = this.items.find { it == favoriteStatus.itemId }
val updatedItem : Item = itemToReplace.copy(favoriteStatus = favoriteStatus)
// Create a copy of ShowContent state with the updated item
this.copy(items = this.items.copyAndReplace(itemToReplace, updatedItem) )
}
}
)
}
}
}
...
}
First, let’s take a look at onActionStartStateMachine() public API.
It has 3 parameters.
Multiple overloads exists, and in our case the one with only 2 parameters is enough.
Nevertheless, let’s explain all 3 parameters of onActionStartStateMachine():
stateMachineFactory: (Action, State) -> FlowReduxStateMachine: Inside this block you create a state machine. In our case we create aFavoriteStatusStateMachine. You have access to the current state of theItemListStateMachineand theActionthat has triggeredonActionStartStateMachine()stateMapper: (State<T>, StateOfNewStateMachine) -> ChangedState<T>: we need to have a way to combine the state of the newly started state machine with the one of the “current” state machine. In our case we need to combineItemListStateMachine’s state withFavoriteStatusStateMachine’s state. That is exactly whatstateMapperis good for. The difference is thatItemListStateMachineprovides aState<T>to thestateMapper(first parameter) whereasFavoriteStatusStateMachineprovides the currentFavoriteState(notState<FavoriteState>). The reason is that at the end we need to get a compatible state forItemListStateMachineand that is what we need to do through the already knownState<T>.override()orState<T>.mutate()methods.actionMapper: (Action) -> OtherStateMachineAction?: We didn’t need this in our example above becauseFavoriteStatusStateMachineis not dealing with any Action. In theory, however, we need to “forward” actions fromItemListStateMachinetoFavoriteStatusStateMachine. But since the actions of the 2 state machines could be of different types, we would need to map an action type ofItemListStateMachineto another action type ofFavoriteStatusStateMachineornullif not all actions are supposed to be handled byFavoriteStatusStateMachine. Returningnullin theactionMappermeans the action is not forwarded toFavoriteStatusStateMachine. Again, this is not needed here in this example, but in theory could be needed in other use cases.
You may wonder what the lifecycle of the state machine started from onActionStartStateMachine() looks like:
- the state machine start (in our case FavoriteStatusStateMachine) will be kept as long alive as the surrounding inState<State> holds true. This works just like the other DSL primitives work (like on<Action>). In our example a FavoriteStatusStateMachine is canceled when ItemListStateMachine transitions away from ShowContent state.
- Every time an Action that is handled by onActionStartStateMachine() is dispatched, then the stateMachineFactory is invoked and a new state machine gets started. Important is that actions are distinguished by it’s .equals() method. In our example ToggleFavoriteItemAction(itemId = 1) and ToggleFavoriteItemAction(itemId = 2) are two different Action because ToggleFavoriteItemAction.equals() also takes itemId into account. Therefore, with 2 instances of FavoriteStatusStateMachine are started, one for itemId = 1 and one for itemId = 2.
- if the .equals() same ToggleFavoriteItemAction(itemId = 1) gets dispatched, then the previous started state machine gets canceled and a new one starts (with the latest action as trigger). There is always only 1 state machine for the same action as trigger running.
Make DSL even more readable with custom DSL additions¶
In the previous section we have introduced onActionStartStateMachine() but it is quite a bit of code in our otherwise nicely readable spec { } block:
spec {
inState<Loading> {
onEnter { loadItemsAndMoveToContentOrError(it) }
}
inState<Error> {
on<RetryLoadingAction> { action, state ->
state.override { Loading }
}
collectWhileInState(timerThatEmitsEverySecond()) { value, state ->
decrementCountdownAndMoveToLoading(value, state)
}
}
inState<ShowContent> {
// Quite a bit of unreadable code
onActionStartStateMachine(
stateMachineFactory = {
action: ToggleFavoriteItemAction, stateSnapshot : ShowContent ->
val item : Item = stateSnapshot.items.find { it == action.itemId}
// create and return a new FavoriteStatusStateMachine instance
FavoriteStatusStateMachine(item, httpClient)
},
stateMapper = {
itemListState : State<ShowContent>, favoriteStatus :FavoriteStatus ->
itemListState.mutate {
val itemToReplace : Item = this.items.find { it == favoriteStatus.itemId }
val updatedItem : Item = itemToReplace.copy(favoriteStatus = favoriteStatus)
// Create a copy of ShowContent state with the updated item
this.copy(items = this.items.copyAndReplace(itemToReplace, updatedItem) )
}
}
)
}
}
We can do better than this, right?
How?
Which Kotlin extension functions and receivers.
The receiver type is InStateBuilderBlock is what inState<S> is operating in.
spec {
inState<Loading> {
onEnter { loadItemsAndMoveToContentOrError(it) }
}
inState<Error> {
on<RetryLoadingAction> { action, state ->
state.override { Loading }
}
collectWhileInState(timerThatEmitsEverySecond()) { value, state ->
decrementCountdownAndMoveToLoading(value, state)
}
}
inState<ShowContent> {
onToggleFavoriteActionStartStateToggleFavoriteStateMachine()
}
}
private fun InStateBuilderBlock.onToggleFavoriteActionStartStateToggleFavoriteStateMachine(){
onActionStartStateMachine(
stateMachineFactory = {
action: ToggleFavoriteItemAction, stateSnapshot : ShowContent ->
val item : Item = stateSnapshot.items.find { it == action.itemId}
// create and return a new FavoriteStatusStateMachine instance
FavoriteStatusStateMachine(item, httpClient)
},
stateMapper = {
itemListState : State<ShowContent>, favoriteStatus :FavoriteStatus ->
itemListState.mutate {
val itemToReplace : Item = this.items.find { it == favoriteStatus.itemId }
val updatedItem : Item = itemToReplace.copy(favoriteStatus = favoriteStatus)
// Create a copy of ShowContent state with the updated item
this.copy(items = this.items.copyAndReplace(itemToReplace, updatedItem) )
}
}
)
}
onEnterStartStateMachine()¶
Similar to onActionStartStateMachine() FlowRedux provides a primitive to start a state machine onEnter{ ... }
The syntax looks quite similar to onActionStartStateMachine():
spec {
inState<MyState>{
onEnterStartStateMachine(
stateMachineFactory = { stateSnapshot : MyState -> SomeFlowReduxStateMachine() },
stateMapper = { state : State<MyState>, someOtherStateMachineState : S ->
state.override { ... }
}
)
}
}