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

CoroutineScope

Jparrgam opened this issue · comments

Hi, is it possible to call suspend function in on ?

ihave this state machine

inStateWithCondition(isInState = { state -> state.loading }) {
                onEnterEffect {
                    val questionsResult = complianceQuestionsUseCase("co")
                    Log.e("onEnterEffect", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    OverrideState(
                        ComplianceState(
                            showContent = false,
                            loading = false,
                            error = true
                        )
                    )
                }

                onEnter {
                    val questionsResult = complianceQuestionsUseCase("co")

                    Log.e("onEnter", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    OverrideState(
                        ComplianceState(
                            showContent = false,
                            loading = false,
                            error = true
                        )
                    )
                }

                on<GetQuestionsAction> { action, _ ->
                    val questionsResult = complianceQuestionsUseCase(action.country)

                    Log.e("GetQuestionsAction", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    OverrideState(
                        ComplianceState(
                            showContent = false,
                            loading = false,
                            error = true
                        )
                    )
                }
            }

in the onEnter and onEnterEffect methods the coroutine is canceled a first time then it enters the methods again and the normal call is made but in the on method the coroutines are always canceled

kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@b8540dc
on<GetQuestionsAction> { action, _ ->
                    val questionsResult = complianceQuestionsUseCase(action.country)

                    Log.e("GetQuestionsAction", "$questionsResult")

                    if (questionsResult is UIState.Success) {
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    NoStateChange
                }

is cancelable but add GlobalScope.Launch is coroutine sucess

GlobalScope.launch {
                        val questionsResult = complianceQuestionsUseCase(action.country)

                        Log.e("GetQuestionsAction", "$questionsResult")

                        if (questionsResult is UIState.Success) {
                            OverrideState(
                                ComplianceState(
                                    showContent = true,
                                    loading = false,
                                )
                            )
                        }
                    }

I am not sure if I fully understand your question but:

  1. yes you can call suspend functions from on (I believe this was your question, isn't it?)
  2. The reason (if I understand your code snippets correctly) why you on<GetQuestionsAction> block os cancelled (without using GlobalScope) is because this is how FlowRedux is designed: as soon as the condition isInState doesn't hold anymore all work inside inStateWithCondition { ... } is canceled.because your FlowReduxStateMachine has "left the state". Just by looking at your code is hard to tell what exactly happened but my guess is that your onEnter block completes fast and then does OverrideState ( ComplianceState(..., loading = false, ...) ). Thus your statemachine has left the state as its condition inStateWithCondition(isInState= { state -> state.loading == true}) doesn hold anymore. This any work i.e. the block of on<GetQuestionsAction> gets cancelled. So this is the expected behavior of FlowReduxStateMachine. With GlobalScope.launch{...} you "escape" the FlowRedux behavior because you launch a coroutine that is outside of FlowReduxStateMachine's scope. Therefore, it doesn't get canceled even if the isInState condition doesn't hold anymore. This is highly discouraged!
  1. i think you have written compiling but incorrect code (we should work on writing some lint rules) or in general misunderstood a key concept of FlowRedux.
onEnter {
                    val questionsResult = complianceQuestionsUseCase("co")

                    if (questionsResult is UIState.Success) {
                        // <-- this if block gets executed when if statement holds but it doesnt do anything meaningful
                        OverrideState(
                            ComplianceState(
                                showContent = true,
                                loading = false,
                            )
                        )
                    }

                    OverrideState( // <--- this is always excutes and since it is the last statement of this lambda block it is the return value and therefore changes the state as described below (thus if statement.above has no meaning)
                        ComplianceState(
                            showContent = false,
                            loading = false,
                            error = true
                        )
                    )
                }

If you take a look at the definition of onEnter( (State) -> ChangeState<State>) (same is true for on<Action>) you will notice that there is really one return value of the onEnter{ ... } block expected. Thus the last statement in your onEnter{...} block is the return value (that is just how Kotlin works). Perhabs you have forgotten an else in your code? I.e.

if (questionsResult is UIState.Success) OverrideState (...) 
else OverrideState ( ... )
  1. Just in case it was not clear: The difference between onEnter and onEnterEffect is that onEnter( (State) -> ChangeState<State> expects a return value and changes the state accordingly, whereas onEnterEffect is defined as follows: onEnterEffect( (State) -> Unit). so onEnterEffect doesn't change state. "Effects" in FlowRedux have an entirely different use case. They are good for doing something that doesn't change state. A typical usecase is triggering Navigation. let's say you just want to navigate from a screen that is powered by FlowReduxStateMachine to another screen. Then you typically do that by using effects as you don't want to change the state, but still react on an Action.
onActionEffect<FooAction> { action, state -> 
     navigator.navigateToOtherScreen()
     // doesn't change state, just do some "effect" without changing state
} 

Hi @sockeqwe thanks for the explanation, I'm going to review my implementation, I'll tell you how it goes, my question is
on is posible send request to api ? example button send Action to state machine and action run request api and update state?

my implementation is with compose my state is

data class ComplianceState(
    val loading: Boolean = true,
    val showContent: Boolean = false,
    val error: Boolean = false,
    val questionsId: String? = null,
    val nextQuestionsId: String? = null,
    val answer: QuestionResult? = null,
    val answerTitle: String = "",
) {
    companion object {
        val LoadingState = ComplianceState(loading = true)
    }
}

Yes, that is of course possible. Some example code:

inStateWithCondition( isInState = { state -> state.loading}) {
   onEnter { state ->
      try {
        val result = api.loadSomeData()
        OverrideState( state.copy ( ... , showContent = true, loading = false)
     } catch (e : Exception) {
       OverrideState( state.copy ( ... , error= true, loading = false)
      }
  }
}

inStateWithCondition( isInState = { state -> state.error }) {
    on<RetryLoadingAction> { _, state -> 
         OverrideState (state.copy(error = false, loading = true) )
    }
}

Btw. I would model different states with sealed classes or sealed interface but it is just a matter of personal preference.

sealed interface State {
   val questionId : String
}

data class Loading (override val questionId : String) : State
data class Error (override val questionId : String) : State
data class ShowContent (
   override val questionId : String,
   val nextQuestionsId: String? = null,
   val answer: QuestionResult
   val answerTitle: String
) : State

then statemachine spec looks like this:

inState<Loading> {
   onEnter { state ->
      try {
        val result = api.loadSomeData()
        OverrideState( ShowContent (...) )
     } catch (e : Exception) {
       OverrideState( Error(state.questionId) )
      }
  }
}

inState<Error> {
    on<RetryLoadingAction> { _, state -> 
         OverrideState (LoadingState(state.questionId))
    }
}

I'm closing this issue now. Feel free to reopen it if you stumble upon new issues ...

Hi @sockeqwe, I made the changes as you advised me, but I have a problem when I make an http call, I'm getting a courutine is canceled error, I share my code with you

state machine

spec {
            inState<LoadingState> {
                on<GetQuestionsAction> { action, _ ->
                    Log.e(
                        "ComplianceMachineState",
                        "LoadingState GetQuestionsAction: ${action.country}"
                    )
                    OverrideState(GetQuestionsAnswerState(action.country))
                }
            }

            inState<GetQuestionsAnswerState> {
                onEnter { state ->
                    Log.e(
                        "ComplianceMachineState",
                        "GetQuestionsAnswerState"
                    )
                    try {
                        val result = complianceQuestionsUseCase(state.country) //suspend function call return error kotlinx.coroutines.JobCancellationException: StandaloneCoroutine was cancelled; job=StandaloneCoroutine{Cancelling}@b74dc27
                        if (result is UIState.UnExpectedError) {
                            OverrideState(GetQuestionsAnswerErrorState(state.country))
                        } else {
                            OverrideState(AnswerShowContentState())
                        }
                    } catch (e: Exception) {
                        OverrideState(GetQuestionsAnswerErrorState(state.country))
                    }
                }
            }

            inState<AnswerShowContentState> {
                onEnter {
                    Log.e("ComplianceMachineState", "AnswerShowContentState")
                    NoStateChange
                }
            }

            inState<GetQuestionsAnswerErrorState> {
                onEnter { state ->
                    Log.e(
                        "ComplianceMachineState",
                        "GetQuestionsAnswerErrorState onEnter: ${state.country}"
                    )
                    OverrideState(GetQuestionsAnswerState(state.country))
                }
            }
        }

prints logs, no enter in GetQuestionsAnswerErrorState

2022-05-02 13:36:09.593 3080 E LoadingState GetQuestionsAction: co
2022-05-02 13:36:13.471 3080 E GetQuestionsAnswerState

my composable

@Composable
fun TestFlowReduxStateMachine(
    stateMachine: ComplianceMachineState,
) {
    val (state, dispatchAction) = stateMachine.rememberStateAndDispatch()


    LaunchedEffect(Unit) {
        dispatchAction(GetQuestionsAction("CO"))
    }
}

onEnter coroutine call complianceQuestionsUseCase is cancelled