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

[Question] State Collection On Multiple Screens

igorwojda opened this issue · comments

Is it save co collect state on multiple screens from the same (injected) instance of state machine (potentially on different threads) and using both collect and rememberState?

e.g.

//  Collection at custom class
MainScope().launch {
            stateMachine.state.collect { state ->
                
            }
        }

//  Collection at screen
val state = viewModel.stateMachine.rememberState()

Hello,
not 100% sure what your setup looks exactly like but every call to state.collect {...} let the state machine start from initial state: That is just how Flow<T> works.

stateMachine.state.collect { state -> ... }

is the same as

val myMflow = flowOf {
   emit(1) 
   delay(10_000) // wait for 10 seconds
   emit(2)
}

launch {
    myFlow.collect { println("first: $it") }
}

// "Sharing" myFlow variable and calling collect again
launch {
    delay (1_000) // wait for 1 second
    myFlow.collect { println("second: $it") }
}

The output is:

First 1
Second 1
First 2
Second 2

So you are not really "reusing" and sharing myflow. Everytime you call flow.collect { ... } a new Flow<T> instance is created and that starts initial state (in case of the example above: myFlow starts with emit(1))

The same is happening with FlowRedux state machines:

val myStateMachine = SomeFlowReduxStateMachine(initalState = MyInitialState).

launch {
     myStateMachine.state.collect { state -> println("first: $state")
     delay (100) // just to mimic user input happened somewhen later:
     myStateMachine.dispatch(MyActionThatTriggersStateTransitionToSecondState)
}

launch {
   delay(2_000) // wait for 2 seconds so that MyActionThatTriggersStateTransitionToSecondState is dispatched:
   myStateMachine.state.collect { state -> println("second: $state")
}

Guess what the output is?

first MyInitialState
first SecondState
second MyInitialState

Again, the reason is that state.collect { ... } creates a new instance of Flow<YourState> and starts at initial state. again.

So sharing the myStateMachine instance is not enough to really share the state, the way it is not enough to share the variable myFlow in the previous example.

To answer your original question:

Is it save co collect state on multiple screens from the same (injected) instance of state machine (potentially on different threads) and using both collect and rememberState?

Yes, it is save, because you are creating a new Flow<MyState> instance every time you call myStatemachine.state.collect { ... } (I hope you understood why from the examples mentioned above.) If you hold internal variables inside your FlowReduxStateMachine (which you should not, all variables should be part of the MyState class and not a private val inside your MyFlowReduxStateMachine class`) then you need to take care of synchronizing these private variables in a coroutine "thread safe way".

How would you like to overcome this? Probably write a wrapper around your FlowReduxStateMachine that uses StateFlow.

I think @gabrielittner has such a FlowReduxStateMachine implementation that wraps around StateFlow. Maybe he can share this implementation in case it helps?

Also, one more thing to watch out for:
One flaw, however, is that calling mystatemachine.dispatch(...) actually dispatches the action to all previously instantiated and still active "Flows". This is something I think we should consider to change in a FlowRedux 2.0 release.

Maybe in a 2.0 release version the class FlowReduxStateMachine should become something like a FlowReduxStateMachineFactory to make this thing even more clear:

// potential FlowRedux 2.0 public API, just in my head, not synced or aligned with any other maintainer yet
class MyFlowReduxStateMachineFactory : FlowReduxStateMachineFactory<MyState, MyAction> {
   init {
      spec { 
          inState { ... } // all the existing stuff from today
      }
   }
   
   // instead of a val state : MyState and suspend dispatch(...) function FlowReduxStateMachineFactory only has a  `newInstance()` function
   fun newInstance() :  Pair<Flow<MyState>, DispatchFunction> = ...
}

The usage would be:

val myStateMachineFactory = MyFlowReduxStateMachineFactory()

state, dispatch = myStateMachineFactory.newInstance()

launch {
   state.collect { state -> println(state) }
   dispatch(MyAction)
}

In that case dispatch() is guaranteed to dispatch only to the same "Flow Instance" that got returned from the MyFlowReduxStateMachineFactory.newInstance()