https://kotlinlang.org logo
#orbit-mvi
Title
# orbit-mvi
v

vicky7230

03/16/2024, 10:45 AM
Hi @Guilherme Delgado I read your article here: https://proandroiddev.com/managing-the-ui-state-by-using-a-finite-state-machine-and-mvi-architecture-36d84056c616 I am trying to implement same in my sample project
I have written the following logic to make a network call:
Copy code
@HiltViewModel
class HomeViewModel @Inject constructor(
    private val repository: Repository
) : ViewModel(), ContainerHost<HomeUiState, Nothing> {

    override val container =
        container<HomeUiState, Nothing>(HomeUiState())

    private val stateMachine = StateMachine.create<HomeState, HomeEvent, HomeSideEffect> {
        initialState(HomeState.Idle)

        state<HomeState.Idle> {
            on<HomeEvent.OnLoading> {
                transitionTo(HomeState.Loading, HomeSideEffect.Loading)
            }
        }

        state<HomeState.Loading> {
            on<HomeEvent.OnError> {
                transitionTo(HomeState.Error, HomeSideEffect.Error(it.error))
            }
            on<HomeEvent.OnSuccess> {
                transitionTo(HomeState.Success, HomeSideEffect.Success(it.list))
            }
        }

        state<HomeState.Success> {}
        state<HomeState.Error> {
            on<HomeEvent.OnLoading> {
                transitionTo(HomeState.Loading, HomeSideEffect.Loading)
            }
        }

        onTransition {
            val validTransition = it as? StateMachine.Transition.Valid ?: return@onTransition

            Timber.tag("FSM").e("\n=================================================")
            Timber.tag("FSM").e("From State: ${it.fromState}")
            Timber.tag("FSM").e("Event Fired = ${it.event::class.simpleName}")
            Timber.tag("FSM").e("State Transitioned to: ${it.toState}")

            when (val effect = validTransition.sideEffect as HomeSideEffect) {
                is HomeSideEffect.Loading -> intent { reduce { state.copy(loading = true) } }
                is HomeSideEffect.Error -> intent {
                    reduce {
                        state.copy(
                            loading = false,
                            error = effect.error
                        )
                    }
                }

                is HomeSideEffect.Success -> intent {
                    reduce {
                        state.copy(
                            error = "null",
                            loading = false,
                            todos = effect.list
                        )
                    }
                }
            }
        }
    }

    init {
        getTodos()
    }

    fun getTodos() {
        viewModelScope.launch {
            repository.getTodos().collect {
                updateState(it)
            }
        }
    }

    private suspend fun updateState(networkResult: NetworkResult<JsonElement>) {
        when (networkResult) {
            is NetworkResult.Loading -> {
                stateMachine.transition(HomeEvent.OnLoading)
            }

            is NetworkResult.Success -> {
                val listJson = networkResult.data
                withContext(<http://Dispatchers.IO|Dispatchers.IO>) {
                    val deserializedList = parseTodoList(listJson)
                    stateMachine.transition(HomeEvent.OnSuccess(deserializedList))
                }
            }

            is NetworkResult.Error -> {
                var errorMessage = networkResult.message
                stateMachine.transition(HomeEvent.OnError(errorMessage!!))
            }

            is NetworkResult.Exception -> {
                var exceptionMessage = networkResult.throwable.localizedMessage
                stateMachine.transition(HomeEvent.OnError(exceptionMessage!!))
            }
        }
    }
}
Copy code
sealed class HomeState {
    data object Idle : HomeState()
    data object Loading : HomeState()
    data object Error : HomeState()
    data object Success : HomeState()
}

sealed class HomeEvent {
    data object OnLoading : HomeEvent()
    data class OnError(val error: String) : HomeEvent()
    data class OnSuccess(val list: List<Todo>) : HomeEvent()
}

sealed class HomeSideEffect {
    data object Loading : HomeSideEffect()
    data class Error(val error: String) : HomeSideEffect()
    data class Success(val list: List<Todo>) : HomeSideEffect()
}

data class HomeUiState(
    val loading: Boolean = false,
    val error: String = "null",
    val todos: List<Todo> = emptyList()
)
I am using both mvi orbit and tinder state machine I just wanted to know your thoughts on this code is this the right way of doing things?
cc: @Mikolaj Leszczynski
g

Guilherme Delgado

04/01/2024, 8:56 AM
Hello, sorry for the late reply, I was on vacations. As soon as I have the time I’ll look into it 😉
v

vicky7230

04/01/2024, 3:25 PM
Sure
g

Guilherme Delgado

04/02/2024, 10:23 AM
Hello, it looks OK to me, If I were to implement something similar, I would include everything in the FSM, in other words, you only use the FSM to change UI state, I would use it also to perform the network calls. Example:
Copy code
...
        state<LandingState.State.Idle> {
            on<LandingState.Event.OnCheckedIn> {
                logState(this, LandingState.Event.OnCheckedIn)
                transitionTo(LandingState.State.Finish, LandingState.SideEffect.NavigateHome)
            }
        state<LandingState.State.Finish> {}
Copy code
private val stateMachine = LandingStateMachine { effect ->
        when (effect) {
            is LandingState.SideEffect.ShowAbout -> containerHost.intent { postSideEffect(ShowAbout) }
            is LandingState.SideEffect.NavigateHome -> containerHost.intent { postSideEffect(NavigateToHome) }
            is LandingState.SideEffect.CheckIn -> checkIn(effect.tokens.token)
            is LandingState.SideEffect.Error -> effect.error?.let { containerHost.intent { parseError(it) } }
            ...
        }
    }
Copy code
private fun checkIn(token: String) {
        containerHost.intent {
                with(stateMachine) {
                    when (val result = sessionManager.checkIn(token)) {
                        is SessionManager.CheckInResult.Success -> {
                            reduce { state.copy(hasSession = true) }
                            transitionOn(LandingState.Event.OnCheckedIn)
                        }
                        is SessionManager.CheckInResult.Error -> {
                            if (result.exception.isTimeoutException()) {
                                transitionOn(LandingState.Event.OnCheckInTimeout())
                            } else {
                                transitionOn(LandingState.Event.OnError(result.exception))
                            }
                        }
                    }
                }
        }
    }
This way my “screen” is fully protected by the FSM, be it UI changes or logic changes
I would also only use FSM in screens that need to have this navigation strictly “protected/validated”, otherwise you could end up in having a lot of boilerplate code for simples things.
Tinder uses it for in-app purchases flows, for example, it’s a good candidate
3 Views