Search code examples
androidkotlinretrofitkotlin-coroutines

How to handle timeouts in Retrofit inside the Fragment


I want to show an error message when timeout occurs, but can't find how to do it without passing the loginProgressBar and loginTimeoutErrorTextview or other UI related variables. Basically keep the UI changes in the Fragment.

What i do in my loginFragment when pressing login

try {
    loginViewModel.login(
        binding.emailEditText.text.toString(),
        binding.passwordEditText.text.toString()
    )
} catch (e: IOException) { // THIS doesn't work
    binding.loginProgressBar.visibility = View.GONE
    binding.loginTimeoutErrorTextview.visibility = View.VISIBLE
}

From what i've found the timeout throws an IOException so everything is correct here

loginViewModel.login method:

viewModelScope.launch {
    val securedLoginRequest = encodedRequest(username, password)

    Log.i("API Login", "Sent data: $securedLoginRequest")

    try {
        Log.i("API Login", "login started")
        // _response is a LiveData<String>
        _response.value =
            ApiServiceObject.retrofitService.postLogin(securedLoginRequest)

        Log.i("API Login", "Login successful, token = ${_response.value}")
    } catch (e: IOException) {
        throw e // THIS should theoretically get catched by the block above right?
    } catch (e: Exception) {
        Log.w("API Login", e.toString())
    }
}

Problem is the thrown exception in Block 2 doesn't get catched in Block 1

I've found a workaround by simply passing down loginProgressBar and loginTimeoutErrorTextview to loginViewModel.login but is that alright? Isn't there better ways?

UPD: some clarifications


Solution

  • I've found a way, by using a StateFlow instead of LiveData in the ViewModel as follows:

    private val _loginState = MutableStateFlow<LoginState>(LoginState.Empty)
    val loginState = _loginState.asStateFlow()
    
    sealed class LoginState {
        object Empty : LoginState()
        object Loading : LoginState()
        data class Success(val result: String) : LoginState()
        data class Error(val error: Throwable) : LoginState()
    }
    

    Then responding to it in the Fragment like this:

    lifecycleScope.launch{
        repeatOnLifecycle(Lifecycle.State.STARTED){
            loginViewModel.loginState.collect { state ->
                binding.loginProgressBar.isGone = state !is LoginState.Loading
    
                when (state) {
                    is LoginState.Error -> { doSomething() }
                    is LoginState.Success -> { doSomething() }
                    else -> Unit
                }
            }
        }
    }