freeletics / FlowRedux

Kotlin Multiplatform Statemachine library with nice DSL based on Flow from Kotlin Coroutine's.

Home Page:https://freeletics.github.io/FlowRedux/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

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. additionalStopConidtionis 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?

cc @gabrielittner

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.