Brainstorming: enhance DSL to cover fine granular sub statemachine
sockeqwe opened this issue · comments
The following use case is not really nicely handable by FlowRedux at the moment:
Let's say we have a list of Items that we want to display on screen. So the overall screen state looks something like this:
sealed interface ScreenState
data class ShowListScreenState(val items : List<Item>) : ScreenState
object LoadingState : ScreenState
What if in the UI each Item
that is displayed in the list of Items also has a button. An by pressing that button a http request is done to update something to the backend for this particular item (i.e. mark a item as "Liked" on a facebook alike newsfeed post)?
In other words, each Item
has their own state.
data class Item (val id : Int, val state : ItemState)
enum class ItemState {
IDLE,
LOADING
ERROR,
SUCCESSFUL
}
data class ItemButtonClickedAction (val itemId : Int) : Action
What I would like to propose and Brainstorm here is to take usage of the substate machines concept we already have.
spec {
inState<LoadingState> {
onEnter {
val items = loadItemsFromServer()
OverrideState(ShowListScreenState (items))
}
inState<ShowListScreenState> {
// NEW DSL primitive
onActionStartSubstateMachine<ItemButtonClickedAction>(
stateMachineFactory = { action: ItemButtonClickedAction, state : ShowListScreenState ->
ItemButtonStateMachine( action.itemId)
},
actionMapper = { it }, // Forward future incoming actions to the substate machine (not needed in this example)
additionalStopCondition= { state : ShowListScreenState, stateFromItemButtonStateMachine : ItemState ->
// Invoked after stateMapper ran
stateFromItemButtonStateMachine != ItemState.LOADING
},
stateMapper = { state : ShowListScreenState , stateFromItemButtonStateMachine : ItemState,
MutateState<ShowListScreenState, ScreenState> {
copy(items = items.replaceItem(Item(originatedAction.id, stateFromItemButtonStateMachine)))
}
}
}
class ItemButtonStateMachine (val itemId : Int) : FlowReduxStateMachine<Action, ItemState> {
init {
spec {
inState<LOADING>
onEnter {
val succesful : Boolean = makeHttpRequest(itemId)
OverrideState (
if (successful) SUCCESSFUL
else ERROR)
}
}
}
}
}
So basically you can start a "substatemachine" from an action. We have already known concepts (from already existing statemachine()
DSL construct) like stateMachineFactory
but alsostateMapper()
and actionMapper()
The remaining question for me is when does a sub statemachine stops. As always it should stop when the surounding inState<...>
condition (of parent statemachine) doesn't hold anymore but we may additionally need some more fine grained control. This is why I would propose to introduce additionalStopConidtion: (ParentStateMachineState, SubStateMachineState) -> Boolean
. additionalStopConidtion
is invoked after a state change of either parentStateMachine or subStateMachine (so it is called after reducer or stateMapper
did run).
If additionalStopCondition
returns true the child statemachine will be stopped (collection of state flow of child statemachine is cancelled; it is not possible to resume child state machine).
as usually ExecutionPolicy
can be applied (thus ExecutionPolicy.CANCEL_PREVIOUS
can cancel substatemachine as well).
the same concept of starting substatemachine can also be applied to collectWhileInState
and onEnter
. in fact current stateMachine()
call is actually already onEnterStartSubStatemachine()
.
Alternative solutions
instead of onActionStartSubstateMachine()
and the like we could also go a different route of introducing additional primitives for inState
to make define state changes even mor fine granular detectable and triggerable.
maybe something like (pseudocode / non functional DSL)
inState<ShowListScreenState> { state –>
// collectWhileInState () + on<Action> are available of course
for (item in state.items){
inState(condition = { item –> item.state == ItemState.LOADING }) {
onEnter { item –>
val succesful : Boolean = makeHttpRequest(item.id)
OverrideState (
if (successful) SUCCESSFUL
else ERROR
}
}
}
but that is an entirely shift of DSL as we use it right now. right now DSL is basically a one-time builder that executes one time at init but what I am proposing above is more like dynamic execution of the DSL over and over again on state changes like what Jetpack Compose does, so relates to #209.
I think for a FlowRedux 1.0 release going with first option (onActionStartSubstateMachine()
) should be sufficient but I totally can see how jetpack compose (greetings to square molecule but also would be great to have kmp support, native jetbrains compose is at least to me cumbersome to work with at the moment) would allow us more flexibility and a sharp powerful tool to build even better state machines. I'm just not sure right now how much of this flexibility and dynamic-ism we need to guide users of FlowRedux in the dirction we want, powerful enough, but also specific and expressive enough to not have options to do things in multiple ways and shoot yourself in the foot. Also current stateamachine()
(to start substatemachine) seems to work good for us, so if we only need one mor way to start sub statemachines on Actions, maybe that is actually all we need at least for now / FlowRedux 1.0.
What do you think?
Any thoughts or concerns regarding the first proposal (onActionStartSubstateMachine
) @gabrielittner @fraherm @snappdevelopment ?
Otherwise I will actually start prototyping this ...
I like the first approach 👍
I'm starting working on this this week
Just for reference: I'm working on it on this branch:
https://github.com/freeletics/FlowRedux/tree/action_start_statemachine
Hope to be able to open a PR before Easter.